File Coverage

blib/lib/Mail/SMTP/Honeypot.pm
Criterion Covered Total %
statement 271 529 51.2
branch 121 326 37.1
condition 21 104 20.1
subroutine 38 60 63.3
pod 2 37 5.4
total 453 1056 42.9


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             package Mail::SMTP::Honeypot;
3              
4             # Notes to curious readers:
5             #
6             # This module was cobbled together in a couple of days out of another
7             # project that is neatly partitioned into pieces that have a good
8             # organization. Everything from there was pretty much dumped in this one
9             # file. Sorry 'bout that ;-)
10             #
11             # I was a lot more interested in having it work quickly than making it neat.
12             #
13             # Michael
14             #
15 7     7   48034 use strict;
  7         13  
  7         1632  
16             #use diagnostics;
17             #use lib qw(blib lib);
18              
19 7     7   8107 use Data::Dumper;
  7         99467  
  7         602  
20 7         1391 use Net::DNS::Codes qw(
21             T_PTR
22             C_IN
23             BITS_QUERY
24             RD
25             NS_PACKETSZ
26             HFIXEDSZ
27             QUERY
28             NOERROR
29             NXDOMAIN
30             SERVFAIL
31 7     7   6623 );
  7         12517  
32 7         892 use Net::NBsocket qw(
33             open_udpNB
34             open_listenNB
35             accept_NB
36             inet_aton
37             inet_ntoa
38             sockaddr_in
39             set_so_linger
40 7     7   5583 );
  7         190626  
41 7         898 use Net::DNS::ToolKit qw(
42             gethead
43             newhead
44             get_ns
45 7     7   6696 );
  7         75900  
46             #use Net::DNS::ToolKit::Debug qw(
47             # print_head
48             # print_buf
49             #);
50 7     7   6558 use Net::DNS::ToolKit::RR;
  7         29145  
  7         247  
51 7         64 use POSIX qw(
52             EINTR
53             EWOULDBLOCK
54 7     7   49 );
  7         16  
55 7         643 use Proc::PidUtil qw(
56             if_run_exit
57             is_running
58             get_script_name
59             make_pidfile
60             zap_pidfile
61 7     7   9528 );
  7         5815  
62 7         456 use Sys::Hostname::FQDN qw(
63             fqdn
64 7     7   5806 );
  7         7077  
65 7         3032 use Unix::Syslog qw(
66             :macros
67             openlog
68             syslog
69 7     7   5616 );
  7         15689  
70 7     7   51 use vars qw($VERSION @EXPORT @ISA);
  7         15  
  7         67879  
71             require Exporter;
72              
73             @ISA = qw(Exporter);
74              
75             $VERSION = do { my @r = (q$Revision: 0.11 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r };
76              
77             @EXPORT = qw(
78             run_honeypot
79             );
80              
81              
82             # private file scoped variables
83              
84             my($me,$threads,$dns,$dnshost,$dnsport,$dnsaddr,$deny,$hostname,$laddr,
85             $port,$delay,$config,$syslog,$verbose,$DNStimeout,$maxthreads,$maxcmds,
86             $LOG,$DNSfileno,$disconnect,%Commands,$unique,$log_facility,%subref
87             );
88             my $CRLF = "\r\n";
89              
90             my @IDarray = ('a'..'z','A'..'Z',(0..9));
91              
92             =head1 NAME
93              
94             Mail::SMTP::Honeypot -- Dummy mail server
95              
96             =head1 SYNOPSIS
97              
98             use Mail::SMTP::Honeypot;
99              
100             run_honeypot($config)
101              
102             =head1 DESCRIPTION
103              
104             B is a perl module that appears to provide all the
105             functionality of a standard SMTP server except that when the targeted
106             command state is detected (default DATA), it terminates the connection with
107             a temporary failure and the response:
108              
109             421 Service not available, closing transmission channel
110              
111             The purpose of this module is to provide a spam sink on a tertiary MX host.
112             The module daemon is run on an MX host with a very high priority number
113             specified in it's DNS record. i.e.
114              
115             some_mail_domain.com IN MX 9999 lastmx.servicedomain.com.
116              
117             Since many spammers target this mail server in the hope that its
118             configuration and/or security is not as strong or well maintained as the
119             primary mail host for a domain. In the off chance that a real message is
120             sent to the server, the TEMPORARY failure code will simply make the sending
121             host retry later -- probably with the lower priority numbered host.
122             Meanwhile, the server target by the spam source has its resources consumed
123             by B.
124              
125             Honeypot does not spawn children and holds only a small reference to each
126             thread that it holds to a client, thus consuming minimal resources. It can
127             produce logs useful in analyzing the spam traffic to your site. Using it
128             with a detach in CONN mode is adequate for triggering a companion spam
129             program such as Mail::SpamCannibal while consuming minimum host resources.
130             At our site, we simply run B on the same host as our secondary MX
131             but on a different IP address.
132              
133             Honeypot provides various levels of connection and transaction logging that
134             can be set in the configuration.
135              
136             A delay may be inserted between the receipt of each command and the response
137             from the server daemon to slow down the sending client.
138              
139             =head1 CONFIGURATION
140              
141             Edit the B file to change or set the following:
142              
143             my $config = {
144              
145             # specify the directory for the pid file for this daemon
146             # [required]
147             #
148             piddir => '/var/run',
149              
150             # deny at command state, one of:
151             # CONN EHLO HELO MAIL RCPT DATA
152             # defaults to DATA if not specified
153             # [optional]
154             # deny => 'DATA',
155              
156              
157             # specify the local domain name, defaults to local hostname.
158             # this is probably not what you want if you use virtual IP's
159             # and have a real mail client on the same host. so...
160             # specify the host 'answerback name' here.
161             # [optional]
162             #
163             # hostname => 'my.host.name.com',
164              
165             # specify the IP address to bind the listening port
166             # defaults to ALL interfaces (INADDR_ANY)
167             # [optional]
168             #
169             # ip_address => '1.2.3.4',
170              
171             # listen port -- default 25
172             # this is useful for debugging purposes
173             # [optional]
174             #
175             # port => 25,
176              
177             ## NOTE: see Concurrent Daemon Operation in the
178             ## documentation for setup where another
179             ## mail daemon is running on the same host.
180            
181             # specify the response delay after connect or upon
182             # receipt of an smtp command from the client
183             #
184             # NOTE: if a response is not received
185             # from the client in this time
186             # period, the smptdeny daemon will
187             # issue a 421 response and disconnect
188             # [optional] default 10 seconds
189             #
190             # delay => 10,
191              
192             # syslog facility, one of:
193             # LOG_KERN LOG_USER LOG_MAIL LOG_DAEMON
194             # LOG_AUTH LOG_SYSLOG LOG_LPR LOG_NEWS
195             # LOG_UUCP LOG_CRON LOG_AUTHPRIV LOG_FTP
196             # LOG_LOCAL0 LOG_LOCAL1 LOG_LOCAL2 LOG_LOCAL3
197             # LOG_LOCAL4 LOG_LOCAL5 LOG_LOCAL6 LOG_LOCAL7
198             #
199             # You should not need to change this
200             #
201             # log_facility => 'LOG_MAIL',
202              
203             # syslog log level or (none), one of:
204             # STDERR LOG_EMERG LOG_ALERT LOG_CRIT LOG_ERR
205             # LOG_WARNING LOG_NOTICE LOG_INFO LOG_DEBUG
206             #
207             # NOTE: the command line -d flag overrides
208             # this and sets the level to STDERR
209             # [optional]
210             #
211             syslog => 'LOG_WARNING',
212              
213             # log verbosity
214             # 0 connect only
215             # 1 + To: & From:
216             # 2 + bad commands
217             # 3 + trace execution
218             # 4 + deep trace with sub names
219             # [optional]
220             #
221             verbose => 0,
222              
223             # DNS host, if you do not have a resolver
224             # on your host or for debugging
225             # default: as returned by your resolver for local dns
226             # [optional]
227             # dnshost => 'use.default',
228              
229             # DNS port, useful for debugging
230             # [optional] default 53
231             #
232             # dnsport => 53,
233              
234             # timeout for DNS PTR queries
235             # [optional] default: use 'delay' above
236             #
237             # DNStimeout => 10,
238              
239             # maximum number of connected clients
240             # [optional] default 100
241             #
242             # maxthreads => 100,
243              
244             # maximum number of commands per client
245             # [optional] default 100
246             #
247             # maxcmds => 100,
248              
249             # disconnect the remote after this much time
250             # [optional] default 300 seconds
251             #
252             # disconnect => 300,
253              
254             };
255              
256             =head1 OPERATION
257              
258             Launch the daemon with the command:
259              
260             rc.honeypot.pl [-d] [start | stop | restart]
261              
262             The '-d' flag, this overides the config settings and
263             reports logging to STDERR
264              
265             On some systems it may be necessary to wrap a shell script around
266             rc.honeypot.pl if the path for perl is not in scope during boot.
267              
268             #!/bin/sh
269             #
270             # shell script 'rc.honeypot'
271             #
272             /path/to/rc.honeypot.pl $*
273              
274             A sample shell script is included in the distribution as B
275              
276             NOTE: suggest you test your configuration as follows...
277              
278             Set: verbose => 3,
279             delay => 5,
280              
281             ./rc.honeypot -d start
282              
283             Connect to the daemon from a host not on the same subnet and watch the
284             output from daemon to verify proper operation.
285              
286             Correct the configuration values and ENJOY!
287              
288             =head2 Standalone Operation
289              
290             For operation on a host where B is the only SMTP
291             daemon, the default configuration will work for most installations.
292              
293             =head2 Concurrent Daemon Operation
294              
295             To operate B concurrently with another mail daemon on
296             the same host you must do the following:
297              
298             =item B<1)> add a virtual IP address for the daemon to answer.
299             The IP address in the rc.honeypot.pl config section should be left
300             commented out so that the daemon will bind to INADDR_ANY.
301              
302             In your startup sequence, execute the following: (example for Linux)
303              
304             #/bin/sh
305             #
306             # Edit for your setup.
307             NETMASK="255.255.255.0" # REPLACE with YOUR netmask!
308             NETWORK="5.6.7.0" # REPLACE with YOUR network address!
309             BROADCAST="5.6.7.255" # REPLACE with YOUR broadcast address
310             # assign a virtual IP address
311             IPADDR="5.6.7.8"
312              
313             # assign ethernet device
314             DEVICE="eth0" # REPLACE with your external device
315             LUN="0"
316              
317             # Note: the "real" IP address has no LUN
318             # virtual IP's are assigned LUN's starting with '0'
319             #
320             # i.e. host IP = 5.6.7.1 eth0
321             # virtIP 5.6.7.8 LUN 0 eth0:0
322             # virtIP 5.6.7.9 LUN 1 eth0:1
323              
324             IFACE=${DEVICE}:${LUN}
325             /sbin/ifconfig ${IFACE} ${IPADDR} broadcast ${BROADCAST} netmask ${NETMASK}
326             /sbin/route add ${IPADDR} dev ${IFACE}
327             echo Configuring $IFACE as $IPADDR
328              
329             =item B<2)> run the honeypot daemon on an unused port.
330              
331             Select a high port number that will not interfere with normail operation of
332             the host SMTP daemon or other services on the host.
333              
334             i.e. in the config section of rc.honeypot.pl
335              
336             port => 10025,
337              
338             =item B<3)> add packet filter rules to redirect queries.
339              
340             This example is for IPTABLES on Linux. Similar rules would apply for other
341             filter packages.
342              
343             # allowed chain for TCP connections
344             iptables -N allowed
345             iptables -A allowed -p tcp --syn -j ACCEPT
346             iptables -A allowed -p tcp -m state --state ESTABLISHED,RELATED -j ACCEPT
347             iptables -A allowed -p tcp -j DROP
348              
349             # drop all external packets target on honeypot daemon
350             iptables -t nat -A PREROUTING -p tcp -s 0/0 --dport 10025 -j DROP
351             iptables -t nat -A PREROUTING -p tcp -d 5.6.7.8 --dport 25 -j REDIRECT --to-port 10025
352             # alternate DNAT statement
353             # iptables -t nat -a PREROUTING -p tcp -d 5.6.7.8 --dport 25 -j DNAT --to 5.6.7.8:10025
354              
355             ## if you are running SpamCannibal, add this rule to capture IP's of connecting hosts
356             ## iptables -A INPUT -p tcp -i eth0 --dport 10025 -j QUEUE
357              
358             # allow the internal port to connect
359             iptables -A INPUT -p tcp -s 0/0 --dport 10025 -j allowed
360              
361             =head1 EXPORTS
362              
363             Only one function is exported by Honeypot.pm. This function is called in the
364             rc.honeypot.pl.sample script to launch the B daemon.
365              
366             =over 4
367              
368             =item * run_honeypot($config); # with @ARGV
369              
370             Launch the honeypot daemon.
371              
372             input: config hash
373             returns: nothing (exits)
374              
375             =back
376              
377             =cut
378              
379             sub run_honeypot {
380 0 0   0 1 0 die "arg 1 of run_honeypot must be a hash\n"
381             unless ref $_[0] eq 'HASH';
382 0         0 my $pidfile = &check_run; # check for another running daemon
383 0         0 &check_config; # verify that config array is correct and populated
384 0         0 clean_child(); # double fork a child
385 0 0       0 make_pidfile($pidfile,$$)
386             or die "could not make pidfile '$pidfile' for $$\n";
387 0         0 init_all();
388 0         0 syslog_config();
389 0         0 goto &daemon;
390             }
391              
392             sub usage { # tested by hand
393 0 0   0 0 0 $me = $0 unless $me;
394 0 0       0 print STDERR $_[0],"\n" if $_[0];
395 0         0 print STDERR qq|
396             Syntax: $me start
397             $me stop
398             $me restart
399             $me status
400              
401             -d switch may be added to
402             redirect logging to STDERR
403              
404             |;
405 0         0 exit 1;
406             }
407              
408             sub kill_job {
409 0     0 0 0 my($pidfile) = @_;
410 0         0 my $pid = is_running($pidfile);
411 0 0       0 if ($pid) {
412 0         0 kill 15, $pid;
413 0         0 $pid = 0;
414 0         0 sleep 1;
415             } else {
416 0         0 print STDERR "$me: not running\n";
417             }
418             }
419              
420             # return true on good number
421             sub vld_num { # t => vld_num.t
422 7     7 0 1107 my $num = shift;
423 7   100     45 return $num =~ /\d/ && $num !~ /[\D\s]/;
424             }
425              
426             sub bad_config {
427 0     0 0 0 my($msg) = @_;
428 0         0 print STDERR 'config: ',$msg,"\n";
429 0         0 exit 1;
430             }
431              
432             #=item * $sub_name = who();
433             #
434             #Returns the name of the calling subroutine without the package name.
435             #
436             #=cut
437              
438             sub who {
439 2     2 0 63 (caller(1))[3] =~ /[^:]+$/;
440 2         21 return $& . ': ';
441             }
442              
443             # returns $pidfile if successful or exits
444             #
445             sub check_run {
446 0     0 0 0 my($c) = @_;
447 0 0 0     0 unless ($c->{piddir} && -d $c->{piddir} && -w $c->{piddir}) {
      0        
448 0         0 print STDERR "pid directory not specified or not writable\n";
449 0         0 exit 0;
450             }
451 0         0 $me = get_script_name();
452 0         0 $0 = $me;
453            
454 0 0 0     0 usage('missing command argument(s)') if
      0        
455             (grep($_ eq '-d',@ARGV) && @ARGV < 2) ||
456             @ARGV < 1;
457 0         0 my $pidfile = $c->{piddir} .'/'. $me . '.pid';
458 0         0 foreach(@ARGV) {
459 0 0       0 if ($_ eq 'start') {
    0          
    0          
    0          
    0          
460 0         0 if_run_exit($c->{piddir},"already running\n");
461 0         0 last;
462             }
463             elsif ($_ eq 'stop') {
464 0         0 kill_job($pidfile);
465 0         0 exit 0;
466             }
467             elsif ($_ eq 'restart') {
468 0         0 kill_job($pidfile);
469 0         0 last;
470             }
471             elsif ($_ eq 'status') {
472 0         0 my $pid = is_running($pidfile);
473 0 0       0 if ($pid) {
474 0         0 print STDERR "$pid $me is running\n";
475             } else {
476 0         0 print STDERR "$me not running\n";
477             }
478 0         0 last;
479             }
480             elsif ($_ eq '-d') {
481 0         0 $c->{syslog} = 'STDERR';
482             }
483             else {
484 0         0 usage("unknown command argument '$_'\n");
485             }
486             }
487              
488 0         0 return $pidfile;
489             }
490              
491             ### validate and set configuration defaults
492             #
493             sub check_config {
494             # ip-address
495 4     4 0 546 my($c) = @_;;
496 4 50       23 if ($c->{ip_address}) {
497 0 0       0 bad_config("bad IP address '$c->{ip_address}'")
498             unless $laddr = inet_aton($c->{ip_address});
499             } else {
500 4         13 $laddr = Socket::INADDR_ANY;
501             }
502             # port number
503 4 50       18 if ($port = $c->{port}) {
504 0 0       0 bad_config("bad port number '$port'")
505             unless vld_num($port);
506             } else {
507 4         12 $port = 25;
508             }
509             # delay
510 4 50       32 if ($delay = $c->{delay}) {
511 0 0       0 bad_config("invalid delay '$delay'")
512             unless vld_num($delay);
513             } else {
514 4         10 $delay = 10;
515             }
516             # deny
517 4 50       46 if ($deny = $c->{deny}) {
518 0         0 $deny = uc $deny;
519 0 0       0 bad_config("invalid deny state '$deny'")
520             unless $deny =~ /^(?:CONN|EHLO|HELO|MAIL|RCPT|DATA)$/;
521 0 0       0 $deny = 'HELO|EHLO' if $deny =~ /HELO|EHLO/;
522             } else {
523 4         12 $deny = 'DATA';
524             }
525             # hostname
526 4   66     33 $hostname = $c->{hostname} || fqdn();
527             # syslog
528 4 50       1786 if ($log_facility = $c->{log_facility}) {
529 0         0 $log_facility = uc $log_facility;
530 0 0       0 bad_config("invalid log facility '$log_facility'")
531             unless $log_facility =~ /^(?:LOG_KERN|LOG_USER|LOG_MAIL|LOG_DAEMON|LOG_AUTH|LOG_SYSLOG|LOG_LPR|LOG_NEWS|LOG_UUCP|LOG_CRON|LOG_AUTHPRIV|LOG_FTP|LOG_LOCAL0|LOG_LOCAL1|LOG_LOCAL2|LOG_LOCAL3|LOG_LOCAL4|LOG_LOCAL5|LOG_LOCAL6|LOG_LOCAL7)$/;
532             } else {
533 4         13 $log_facility = 'LOG_MAIL';
534             }
535 4 50       22 if ($syslog = $c->{syslog}) {
536 0         0 $syslog = uc $syslog;
537 0 0       0 bad_config("invalid log request '$syslog'")
538             unless $syslog =~ /^(?:STDERR|LOG_EMERG|LOG_ALERT|LOG_CRIT|LOG_ERR|LOG_WARNING|LOG_NOTICE|LOG_INFO|LOG_DEBUG)$/;
539             }
540             # verbose
541 4 50       37 if ($verbose = $c->{verbose}) {
542 0 0 0     0 bad_config("invalid verbosity '$verbose'")
543             unless vld_num($verbose) && $verbose > 0;
544             ########### DEEP TRACE CODE #############
545 0 0       0 if ($verbose > 3) {
546 0         0 foreach(sort keys %Mail::SMTP::Honeypot::) {
547 0         0 my $subref = \&{"Mail::SMTP::Honeypot::$_"};
  0         0  
548 0         0 $Mail::SMTP::Honeypot::{$_} =~ /[^:]+$/;
549 0         0 $subref{$subref} = $&;
550             }
551             }
552             ########### END DEEP TRACE CODE #############
553             } else {
554 4         13 $verbose = 0;
555             }
556             # dns host
557 4 50       20 if ($_ = $c->{dnshost}) {
558 0 0       0 bad_config("invalid dns hostname '$dnshost'")
559             unless ($dnshost = inet_aton($_));
560             } else {
561 4         49 $dnshost = get_ns();
562             }
563             # dns port
564 4 50       5370 if ($dnsport = $c->{dnsport}) {
565 0 0       0 bad_config("invalid dns port number '$dnsport'")
566             unless vld_num($dnsport);
567             } else {
568 4         9 $dnsport = 53;
569             }
570 4 50       23 if ($dnshost) {
571 4         27 $dnsaddr = sockaddr_in($dnsport,$dnshost);
572             }
573             # DNStimeout
574 4 50       62 if ($DNStimeout = $c->{DNStimeout}) {
575 0 0 0     0 bad_config("invalid DNS timeout '$DNStimeout'")
576             unless vld_num($DNStimeout) && $DNStimeout >= $delay;
577             } else {
578 4         31 $DNStimeout = $delay;
579             }
580             # maxthreads
581 4 50       17 if ($maxthreads = $c->{maxthreads}) {
582 0 0       0 bad_config("invalid maximum client count '$maxthreads'")
583             unless vld_num($maxthreads);
584             } else {
585 4         10 $maxthreads = 100;
586             }
587             # maxcmds
588 4 50       14 if ($maxcmds = $c->{maxcmds}) {
589 0 0       0 bad_config("invalid maximum client count '$maxcmds'")
590             unless vld_num($maxcmds);
591             } else {
592 4         8 $maxcmds = 100;
593             }
594             # disconnect
595 4 50       16 if ($disconnect = $c->{disconnect}) {
596 0 0       0 bad_config("invalid maximum client count '$disconnect'")
597             unless vld_num($disconnect);
598             } else {
599 4         14 $disconnect = 300;
600             }
601             }
602              
603             sub clean_child() {
604 0     0 0 0 my $pid = fork;
605 0 0       0 if ($pid) {
606 0         0 waitpid($pid,0);
607 0         0 exit 0;
608             }
609              
610 0         0 chdir '/'; # allow root dismount
611 0 0       0 open STDIN, '/dev/null' or die "Can't dup STDIN to /dev/null: $!";
612 0 0       0 open STDOUT, '>/dev/null' or die "Can't dup STDOUT to /dev/null: $!";
613              
614 0 0       0 exit 0 if $pid = fork; # double fork to release instantiating terminal
615             }
616              
617             sub _trace {
618 4 100   4   3704 return (wantarray) ? (\$threads,\$dns) : \$threads;
619             }
620              
621             sub init_all() {
622 0     0 0 0 $unique = $$ -1;
623 0         0 $threads = {}; # thread hash
624 0 0       0 die "could not open DNS socket\n"
625             unless ($dns = open_udpNB());
626 0         0 $DNSfileno = fileno($dns);
627 0         0 $threads->{$DNSfileno} = {
628             sock => $dns,
629             alarm => 0,
630             # name => '4.3.2.1.in-addr.arpa',
631             # read => \&dns_rcv
632             };
633 0         0 $dns = {}; # dns transaction hash
634             }
635              
636             sub my_dump {
637 0     0 0 0 my %names;
638 0         0 local *pref = __PACKAGE__ . '::';
639 0         0 foreach(keys %{*pref}) {
  0         0  
640 0         0 $names{'*'.$_} = \&{*pref->{$_}};
  0         0  
641             }
642 0         0 my @d = (
643             $threads => 'threads',
644             $dns => 'dns',
645             );
646 0         0 for ($_=0;$_<@d;$_+=2) {
647 0         0 my $d = new Data::Dumper([$d[$_]],[$d[$_+1]]);
648 0         0 $d->Seen(\%names);
649 0         0 @_ = split(/\n/,$d->Dump);
650 0         0 foreach(@_) {
651 0         0 logit($_ ."\n");
652             }
653             }
654             }
655              
656             sub daemon {
657 0 0 0 0 1 0 unless ($syslog && $syslog eq 'STDERR') {
658 0 0       0 open STDERR, '>/dev/null' or die "Can't dup STDERR to /dev/null: $!";
659             }
660              
661             # initialization complete, log start up message
662 0         0 logit('Initiated...');
663              
664 0         0 my $run = 1;
665 0     0   0 local $SIG{TERM} = sub {$run = 0};
  0         0  
666 0         0 local $SIG{USR1} = \&my_dump;
667 0         0 local $SIG{PIPE} = 'IGNORE';
668              
669 0         0 my $then = time;
670 0         0 my $sock = open_listenNB($port,$laddr);
671 0 0       0 die "could not open listen socket on port $port\n"
672             unless $sock;
673 0         0 my $fileno = fileno($sock);
674 0         0 my $go_listen = $threads->{$fileno} = {
675             sock => $sock,
676             alarm => 0,
677             read => \&newthread,
678             # next => \&next thing to do
679             };
680 0         0 my($rin,$win,$rout,$wout,$delta,$nfound);
681 0         0 while($run) {
682 0         0 $win = $rin = '';
683 0         0 $threads->{$DNSfileno}->{read} = \&dns_rcv; # always armed
684 0         0 foreach(grep(!/\D/,keys %$threads)) { # each thread key
685 0 0       0 vec($rin,$_,1) = 1 if $threads->{$_}->{read}; # set read selects
686 0 0       0 vec($win,$_,1) = 1 if $threads->{$_}->{write}; # set write selects
687             }
688 0         0 $go_listen->{read} = \&newthread; # re-arm listner if it was busy
689 0         0 $nfound = select($rout=$rin,$wout=$win,undef,1); # tick each second
690              
691 0 0       0 if ($nfound > 0) {
    0          
692 0 0       0 do_thread($wout,'write') if $wout;
693 0 0       0 do_thread($rout,'read') if $rout;
694             }
695             elsif ($delta = ($_ = time) - $then) { # timer = next second or more
696 0         0 $then = $_;
697 0         0 my @threads = keys %$threads;
698 0         0 foreach(@threads) { # each receive thread
699 0 0       0 next unless exists $threads->{$_};
700 0         0 my $tptr = $threads->{$_};
701 0 0 0     0 if ($tptr->{alarm} &&
702             ($tptr->{alarm} + $delay) < $then) {
703 0         0 $tptr->{alarm} = time + $disconnect - $delay;
704 0         0 my($logtxt,$go);
705 0 0       0 if ($tptr->{tout}) {
706 0         0 $go = $tptr->{tout};
707 0         0 $logtxt = 'tout ';
708             } else {
709 0         0 $go = $tptr->{next};
710 0         0 $logtxt = 'next ';
711             }
712 0 0       0 if ($verbose > 3) { # deep trace
    0          
713 0 0       0 $logtxt = &who ."delay ended for '$_' $logtxt => ".
714             (exists $subref{$go}) ? $subref{$go} : 'sub ref not defined';
715 0         0 logit($logtxt);
716             }
717             elsif ($verbose > 2) {
718 0         0 logit(&who ."delay ended for '$_'\n");
719             }
720 0         0 $go->($_);
721 0 0 0     0 if (exists $threads->{$_} && ! $threads->{$_}->{tout}) {
722 0         0 $threads->{$_}->{tout} = \&terminate
723             }
724 0         0 last;
725             }
726             }
727 0         0 foreach(keys %$dns) { # each dns thread
728 0 0 0     0 if ($dns->{$_}->{alarm} &&
729             ($dns->{$_}->{alarm} + $delay) < $then) {
730 0 0       0 logit(&who ."dns ended for id $_ for $dns->{$_}->{fileno}\n") unless $verbose < 3;
731 0         0 delete $dns->{$_};
732 0         0 last; # only do one per check for efficiancy
733             }
734             }
735             }
736             }
737 0         0 &close_all;
738 0         0 logit('Exiting...');
739 0         0 closelog();
740 0         0 exit 0;
741             }
742              
743             #
744             # execute a thread based on what the select routine returns
745             # sort used for testing only
746             #
747              
748             sub do_thread { # t => do_thread.t
749 10     10 0 42 my($vec,$op,$sort) = @_;
750 10 50       25 logit(&who . $op) unless $verbose < 3; # trace each thread
751 10         12 my @threads; # use array in case we decide not to use 'goto' at return of this subr
752 10 100       18 if ($sort) {
753 5         43 @threads = sort {$a <=> $b} grep(!/\D/,keys %$threads); # each numeric thread key
  30         50  
754             } else {
755 5         43 @threads = grep(!/\D/,keys %$threads);
756             }
757 10         21 foreach (@threads) { # or if re-entering after read with a deleted thread
758 38 100 66     155 next unless exists $threads->{$_} && $threads->{$_}; # skip killed threads
759 33 100       97 next unless vec($vec,$_,1); # skip inactive threads
760 17 100       53 next unless $threads->{$_}->{$op};
761 5         9 my $go = $threads->{$_}->{$op};
762 5         8 $threads->{$_}->{$op} = undef; # clear vector
763 5 50       14 next unless ref $go; # ignore blank vectors
764 5         10 @_ = ($_);
765 5 50       19 if ($verbose > 3) { # deep trace
    50          
766 0 0       0 my $exsub = (exists $subref{$go}) ? $subref{$go} : 'sub ref not found';
767 0         0 logit(&who ."exec $op for '$_' => $exsub\n");
768             }
769             elsif ($verbose > 2) {
770 0 0       0 logit(&who ."executing $op for '$_'\n") unless $verbose < 3;
771             }
772 5         22 goto $go; # do it and return
773             }
774             }
775              
776             sub writesock { # t => new_rw_sock.t
777 2     2 0 9 my($fileno) = @_;
778 2         10 my $tptr = $threads->{$fileno};
779 2         13 my $bytes = length($tptr->{wargs}) - $tptr->{woff};
780 2         14 $! = 9;
781 2 100       109 my $wrote = syswrite( $tptr->{sock},
782             $tptr->{wargs},
783             $bytes,
784             $tptr->{woff},
785             ) if fileno($tptr->{sock}); # closed filehandles return false
786 2         14 my $logtxt = &who . $fileno .' ';
787 2 100       10 if (defined $wrote) {
788 1         3 $logtxt .= $wrote;
789             }
790             else {
791 1         18 $logtxt .= 'sock error: '. $!;
792             }
793 2 50       13 logit($logtxt) unless $verbose < 3;
794 2 100 50     21 if (defined $wrote) {
    50          
795 1         2 $tptr->{woff} += $wrote;
796 1 50       4 if ($tptr->{woff} == $bytes) { # if complete
797 1         3 my $go = $tptr->{next};
798 1 50       4 unless ($verbose < 4) { # deep trace
799 0 0       0 my $exsub = (exists $subref{$go}) ? $subref{$go} : 'sub ref not found';
800 0         0 logit(&who ."next => $exsub for '$fileno'\n");
801             }
802 1         6 goto $go; # goto the next link
803             }
804             } elsif (sockerror($! || 9)) { # default to bad file descriptor
805 1         10 goto &removethread; # remove thread if there was an error
806             }
807 0         0 $tptr->{write} = \&writesock; # restore write pointer
808             }
809              
810             sub _readsock { # t => new_rw_sock.t
811 3     3   14 my($fileno) = @_;
812 3         12 my $tptr = $threads->{$fileno};
813 3 50       152 my $bytes = sysread( $tptr->{sock},
814             $tptr->{rargs},
815             2048, # limit reads, data is mostly limited to 2048
816             $tptr->{roff}
817             ) if fileno($tptr->{sock}); # closed filehandles return false
818 3 0       22 logit(&who . $fileno .' '. ((defined $bytes) ? $bytes : 'error '. $!))
    50          
819             unless $verbose < 3; # trace
820 3         12 return($tptr,$bytes);
821             }
822            
823             sub readsock { # t => new_rw_sock.t
824 3     3 0 15 my ($tptr,$bytes) = &_readsock;
825 3 100 50     123 if (defined $bytes) { # returns undef on error
    50          
826 2 100       16 goto &removethread
827             unless $bytes; # EOF
828 1         5 $tptr->{alarm} = time; # renew timeout
829 1         6 $tptr->{roff} += $bytes; # bytes read
830 1         4 my $go = $tptr->{next};
831 1 50       7 unless ($verbose < 4) { # deep trace
832 0 0       0 my $exsub = (exists $subref{$go}) ? $subref{$go} : 'sub ref not found';
833 0         0 logit(&who ."next => $exsub for '$_[0]'\n");
834             }
835 1         12 goto $go;
836             } elsif (sockerror($! || 9)) { # default to bad file descriptor
837 0         0 goto &removethread; # detected fatal condition
838             }
839             # probably never get to here
840 1         7 $tptr->{read} = \&readsock; # restore read pointer
841             }
842              
843             #
844             # input: error code
845             # returns: true if error, else false
846             #
847             sub sockerror { # t => sockerror.t
848 6     6 0 805 my($err) = @_;
849 6 100 100     68 return ($err == EINTR || # don't die for interrupts
    100          
850             $err == EWOULDBLOCK) # or while waiting
851             ? 0
852             : ($err) ? 1 : 0;
853             }
854              
855             # remove a thread, closing the socket
856             #
857             # input: threads pointer, fileno
858             # returns: nothing
859             #
860             sub removethread { # t => removethread.t
861 3     3 0 21 my($fileno) = @_;
862 3 50       12 logit(&who . $fileno) unless $verbose < 3;
863 3         12 my $sock = $threads->{$fileno}->{sock};
864 3         19 delete $threads->{$fileno};
865 3 50       349 close $sock if $sock; # don't attempt close on non-existent sock
866             }
867              
868             sub close_all {
869 0     0 0 0 foreach(keys %$threads) {
870 0         0 removethread($_);
871             }
872             }
873              
874             sub newthread { # t => new_rw_sock.t
875 2     2 0 2026378 my($listner) = @_;
876 2 50       26 if ((keys %$threads) > $maxthreads) {
877 0 0       0 logit(&who . "thread pool full\n") unless $verbose < 2;
878 0         0 return;
879             }
880 2         15 $threads->{$listner}->{read} = \&newthread; # restore vector
881 2         30 my($sock,$netaddr) = accept_NB($threads->{$listner}->{sock});
882 2 100       1294 return unless $sock;
883 1         11 my $ipaddr = inet_ntoa($netaddr);
884 1 50       6 unless ($ipaddr) {
885 0         0 close $sock;
886 0         0 return;
887             }
888 1         7 set_so_linger($sock,30); # set linger to 30 seconds, just in case
889 1         20 my $fileno = fileno($sock);
890 1         12 $threads->{$fileno} = {
891             alarm => 1,
892             # cmdcnt => 0, # number of allowed commands
893             # conlog => 0, # connection logged
894             # domain => '', # claims to be this domain
895             ipaddr => $ipaddr, # dot quad
896             # lastc => 'CONN', # last connection state
897             name => '', # smtp host name
898             # next => \&sub, # next sub to exec
899             # proto => 'SMTP', # protocol
900             # read => \&sub, # read sub to exec
901             # rargs => '', # read string
902             # roff => 0, # length
903             sock => $sock, # socket
904             # write => \&sub, # write sub to exec
905             # wargs => ''. # string to write
906             # woff => 0, # offset into write string
907             };
908 1 50       7 if ($deny eq 'CONN') {
909 0         0 $threads->{$fileno}->{next} = \&terminate;
910             } else {
911 1         9 dns_send($fileno,$ipaddr); # initiate a PTR lookup
912 1         11 @{$threads->{$fileno}}{qw(
  1         15  
913             alarm
914             cmdcnt
915             cok
916             domain
917             lastc
918             name
919             next
920             proto
921             wargs
922             )} = (
923             time, # alarm
924             0, # cmdcnt
925             0, # cok
926             '', # domain
927             'CONN', # lastc
928             '', # name
929             \&connOK, # next
930             'SMTP', # proto
931             '220 '. $hostname .' service ready'. $CRLF,
932             );
933             }
934             }
935              
936             sub connOK {
937 0     0 0 0 my($fileno) = @_;
938 0         0 my $tptr = $threads->{$fileno};
939 0         0 $tptr->{cok} = 1; # flag that says this is done
940 0         0 logit('honeypot connect '. $tptr->{name} .'['. $tptr->{ipaddr} .']');
941 0         0 $tptr->{woff} = 0;
942 0         0 $tptr->{next} = \&readSMTP;
943 0         0 $tptr->{tout} = \&write_delay;
944             # $tptr->{alarm} = use previous value
945             }
946              
947             sub terminate { # t => parseSMTP.t
948 0     0 0 0 my($fileno) = @_;
949 0         0 $threads->{$fileno}->{wargs} = '421 Service not available, closing transmission channel'. $CRLF;
950 0 0       0 logit(&who ."sent terminate for '$fileno'\n") unless $verbose < 3;
951 0         0 write_rearm($fileno,\&removethread,1); # immediate terminate
952             }
953              
954             # implementation from rfc 2821
955             #
956             # STATE: allowed commands
957             #
958             # initial: HELO, EHLO, NOOP, HELP, VRFY, RSET, and QUIT
959             #
960             # HELO/EHLO MAIL, HELO, EHLO, NOOP, HELP, VRFY, RSET, and QUIT
961             #
962             # MAIL RCPT, HELO, EHLO, NOOP, HELP, VRFY, RSET, and QUIT
963             #
964             # RCPT RCPT, DATA, EHLO, NOOP, HELP, VRFY, RSET, and QUIT
965             #
966             # DATA {data} .
967              
968             %Commands = (
969             EHLO => \&_EHLO,
970             HELO => \&_HELO,
971             MAIL => \&_MAIL,
972             RCPT => \&_RCPT,
973             DATA => \&terminate,
974             RSET => \&_RSET,
975             VRFY => \&_VRFY,
976             HELP => \&_HELP,
977             NOOP => \&_NOOP,
978             QUIT => \&_QUIT,
979             SEND => \¬imp,
980             SOML => \¬imp,
981             SAML => \¬imp,
982             EXPN => \¬imp,
983             TURN => \¬imp,
984             );
985              
986             sub parseSMTP { # t => parseSMTP.t
987 21     21 0 605 my($fileno) = @_;
988 21         43 my $tptr = $threads->{$fileno};
989 21 50       73 goto &terminate if ++$tptr->{cmdcnt} > $maxcmds;
990 21         32 my $newc = '';
991 21         30 my $smtp_args = '';
992 21 100       104 if ($tptr->{rargs} =~ /^\s*([a-zA-Z]{4})\b/) {
993 18         48 $newc = uc $1;
994 18         39 $smtp_args = lc $';
995             }
996 21         36 my $lastc = $tptr->{lastc};
997 21         35 $tptr->{wargs} = ''; # error text
998 21 100       162 unless ($newc) {
    100          
    100          
    100          
    100          
    50          
999 3         7 $tptr->{rargs} =~ s/[^[\w .-]//g;
1000 3         10 $tptr->{wargs} = '500 5.5.1 Command unrecognized "'. $tptr->{rargs} .'"';
1001             }
1002             elsif (! exists $Commands{$newc}) {
1003 1         5 $tptr->{wargs} = '500 5.5.1 Command unrecognized "'. $1 .'"';
1004             }
1005             elsif ($tptr->{roff} > 512) { # rfc2821 4.5.3.1
1006 1         2 $tptr->{wargs} = '500 5.5.4 Command line too long';
1007             }
1008             elsif ($lastc =~ /(?:CONN|HELO|EHLO)/) {
1009 7 100       25 if ($newc eq 'RCPT') {
    100          
1010 2         5 $tptr->{wargs} = '503 5.0.0 Need MAIL before RCPT';
1011             }
1012             elsif ($newc eq 'DATA') {
1013 2         4 $tptr->{wargs} = '503 5.0.0 Need MAIL command';
1014             }
1015             }
1016             elsif ($lastc eq 'MAIL') {
1017 8 100       28 if ($newc eq 'MAIL') {
    100          
1018 1         3 $tptr->{wargs} = '503 5.5.0 Sender already specified';
1019             }
1020             elsif ($newc eq 'DATA') {
1021 1         3 $tptr->{wargs} = '503 5.0.0 Need RCPT before DATA';
1022             }
1023             }
1024             elsif ($lastc eq 'RCPT') {
1025 1 50       13 if ($newc eq 'MAIL') {
1026 0         0 $tptr->{wargs} = '503 5.5.0 Sender already specified';
1027             }
1028             }
1029              
1030 21 100       58 if ($tptr->{wargs}) { # if there is an error
    100          
1031 11 50       29 logit(&who ."$newc ". $tptr->{wargs}) unless $verbose < 2; # more log info
1032 11         17 $tptr->{wargs} .= $CRLF;
1033 11         34 write_rearm($fileno,\&readSMTP); # send error and return to this routine
1034             } elsif ($newc eq $deny) {
1035 1         4 $tptr->{alarm} = time;
1036 1         5 $tptr->{next} = \&terminate;
1037 1 50       9 logit(&who .'deny '. $newc . $smtp_args) unless $verbose < 3;
1038             } else { # else
1039 9 50       21 logit(&who . $newc . $smtp_args) unless $verbose < 3; # trace success
1040 9         27 $Commands{$newc}->($fileno,$smtp_args,$tptr); # execute the command
1041             }
1042             }
1043              
1044             #
1045             # input: to or from,
1046             # string [to/from: garbage junk email@addy.sufx more junk]
1047             # returns: (error text on error)
1048             # (name,domain) on match
1049             # or (name,{defined+false}) if 'postmaster' by itself
1050              
1051             sub xtract_to_from { # t => parseSMTP.t
1052 8     8 0 17 my $match = lc shift; # 'to' or 'from' or 'vrfy'
1053 8         19 my $string = lc shift; # input string
1054 8 50       50 my $what = ($string =~ /[^\s:]+/) # must have some characters
1055             ? $& : '';
1056 8   50     32 $string = $' || ''; # remainder of string
1057 8 50 33     56 return ('501 5.5.2 Syntax error in parameters scanning "'. $what .'"'. $CRLF)
1058             unless $what eq $match && $string =~ /^:/; # return error if 'to / from' does not match
1059             # or is not terminated with colon
1060 8         17 $string = $'; # snip off colon
1061 8 100       33 if ($string =~ /([\w\.-]+)@([\w\-]+\.[\w\.-]+)/) { # if email addy found
1062 3 50       20 return ('500 5.5.4 User name too long'. $CRLF)
1063             if length($1) > 64; # rfc2821 4.3.5.1
1064 3 50       15 return ('500 5.5.4 Domain name too long'. $CRLF)
1065             if length($2) > 255;
1066 3         13 return ($1,$2);
1067             }
1068 5 50 33     17 return ('postmaster','')
1069             if $string =~ /^\s*?\s*$/ && $match =~/to|vrfy/;
1070 5 50 33     18 return ('','')
1071             if $string =~ /^\s*<\s*>/ && $match =~/from/; # error message returned to ME
1072              
1073             # figure what kind or error to report
1074 5         11 $string =~ s/^\s+//; # waste leading spaces
1075 5         17 @_ = split(/\s+/,$string);
1076 5 50       93 return ('555 5.5.4 "'. $_[1] .'" parameter unrecognized'. $CRLF)
1077             if @_ > 1; # error if there are unknown parameters
1078 5 50       29 return ('553 5.5.4 Domain name required for address "'. $_[0] .'"'. $CRLF)
1079             if $_[0];
1080 0         0 return ('501 5.0.0 Argument required'. $CRLF);
1081             }
1082              
1083             #
1084             # SMTP commands
1085             #
1086             # HELO & EHLO
1087             #
1088              
1089             sub _EHLO { # t => commands.t
1090 2     2   4 push @_, 1;
1091 2         8 goto &_HELO;
1092             }
1093              
1094             sub _HELO { # t => commands.t
1095 5     5   9 my($fileno,$smtp_args,$tptr,$is_EHLO) = @_;
1096 5 100       23 $tptr->{domain} = ($smtp_args =~ /[\w\.-]+/)
1097             ? $& : 'nobody';
1098             # S: 250 hostname ready for {domain}
1099             # (ehlo) 250 HELP
1100 5         30 my $wargs = $hostname . ' ready for '. $tptr->{domain} .' ('. $tptr->{name} .'['. $tptr->{ipaddr} .'])'. $CRLF;
1101 5 100       11 if ($is_EHLO) {
1102 2         6 $tptr->{wargs} =
1103             '250-'. $wargs .
1104             '250 HELP'. $CRLF;
1105 2         3 $tptr->{lastc} = 'EHLO';
1106 2         3 $tptr->{proto} = 'ESMTP';
1107             } else {
1108 3         10 $tptr->{wargs} =
1109             '250 '. $wargs;
1110 3         4 $tptr->{lastc} = 'HELO';
1111             }
1112 5         18 write_rearm($fileno,\&readSMTP);
1113             }
1114              
1115             #
1116             # MAIL
1117             #
1118             # no attempt is made to verify the sender envelope address since
1119             # it is so easy to forge an address that will validate somewhere
1120             #
1121             sub _MAIL { # t => commands.t
1122 3     3   124 my($fileno,$smtp_args,$tptr) = @_;
1123 3         28 my($name,$domain) = xtract_to_from('from',$smtp_args);
1124 3 100       10 unless (defined $domain) {
1125 1         4 $tptr->{wargs} = $name; # 'name' contains the error message when 'domain' is undefined
1126             } else {
1127 2         5 $tptr->{lastc} = 'MAIL';
1128 2         6 $tptr->{wargs} = '250 2.1.0 OK'. $CRLF;
1129 2         7 $tptr->{from} = $name .'@'. $domain;
1130 2         13 $tptr->{msgid} = uniquemsgid();
1131 2 50       8 logit($tptr->{msgid}.': from=<'. $tptr->{from} .'>, relay='. $tptr->{domain}.' ('. $tptr->{name} .'['. $tptr->{ipaddr} .'])')
1132             unless $verbose < 1;
1133             }
1134 3         14 write_rearm($fileno,\&readSMTP);
1135             }
1136              
1137             #
1138             # RCPT && VRFY
1139             #
1140             sub _RCPT { # t => commands.t
1141 2     2   5 push @_, 1;
1142 2         8 goto &_VRFY;
1143             }
1144              
1145             sub _VRFY { # t => commands.t
1146 5     5   13 my($fileno,$smtp_args,$tptr,$is_rcpt) = @_;
1147 5         7 my($name,$domain);
1148 5 100       11 if ($is_rcpt) {
1149 2         6 ($name,$domain) = xtract_to_from('to',$smtp_args);
1150             } else {
1151 3         11 ($name,$domain) = xtract_to_from('vrfy','vrfy:'. $smtp_args);
1152             }
1153 5 100       13 if (defined $domain) {
1154 1 50       7 my $to = ($domain) ? $name .'@'. $domain : $name .'@'. $hostname; # postmaster is by itself without attached domain
1155 1 50       4 $tptr->{lastc} = ($is_rcpt) ? 'RCPT' : 'VRFY';
1156 1         4 $tptr->{wargs} = '250 2.1.5 OK'. $CRLF;
1157 1 50       12 $is_rcpt = ($is_rcpt) ? 'rcpt' : 'vrfy';
1158 1 50       10 logit($tptr->{msgid}.': '. $is_rcpt .'=<'. $to .'>, relay='. $tptr->{domain}.' ('. $tptr->{name} .'['. $tptr->{ipaddr} .'])')
1159             unless $verbose < 1;
1160             }
1161             else {
1162 4         10 $tptr->{wargs} = $name; # this is really the error string from xtract_to_from
1163             }
1164 5         17 write_rearm($fileno,\&readSMTP);
1165             }
1166              
1167             #
1168             # RSET
1169             #
1170             sub _RSET { # t => commands.t
1171 1     1   2 my($fileno,$smtp_args,$tptr) = @_;
1172 1         4 $tptr->{wargs} = '250 2.0.0 OK'. $CRLF;
1173 1         4 goto &soft_reset;
1174             }
1175              
1176             #
1177             # HELP
1178             #
1179             sub _HELP { # t => commands.t
1180 2     2   4 my($fileno,$smtp_args,$tptr) = @_;
1181 2         9 $tptr->{wargs} =
1182             '214-2.0.0 Commands supported are'. $CRLF .
1183             '214-2.0.0 HELO EHLO MAIL RCPT DATA'. $CRLF .
1184             '214 2.0.0 RSET VRFY HELP NOOP QUIT'. $CRLF;
1185 2         8 write_rearm($fileno,\&readSMTP);
1186             }
1187              
1188             #
1189             # NOOP
1190             #
1191             sub _NOOP { # t => commands.t
1192 2     2   4 my($fileno,$smtp_args,$tptr) = @_;
1193 2         7 $tptr->{wargs} = '250 2.0.0 OK'. $CRLF;
1194 2         9 write_rearm($fileno,\&readSMTP);
1195             }
1196              
1197             #
1198             # QUIT
1199             #
1200             sub _QUIT { # t => commands.t
1201 2     2   8 my($fileno,$smtp_args,$tptr) = @_;
1202 2         9 $threads->{$fileno}->{wargs} = '221 2.0.0 '. $hostname .' closing connection'. $CRLF;
1203 2         9 write_rearm($fileno,\&removethread);
1204             }
1205              
1206             #
1207             # DATA
1208             # this is where we disconnect
1209             #
1210             ### REPLACED BY TERMINATE
1211             #
1212             #sub _DATA {
1213             # my($fileno) = @_;
1214             # my $tptr = $threads->{$fileno};
1215             # $tptr->{woff} = 0;
1216             # $tptr->{next} = \&terminate;
1217             # $tptr->{tout} = 0;
1218             # $tptr->{alarm} = time; # wait 'delay'
1219             #}
1220              
1221             #
1222             # notimp
1223             #
1224             sub notimp { # t => parseSMTP.t
1225 5     5 0 11 my($fileno,$smtp_args,$tptr) = @_;
1226 5         11 $tptr->{wargs} = '502 5.5.1 Command not implemented'. $CRLF;
1227 5         16 write_rearm($fileno,\&readSMTP);
1228             }
1229              
1230             sub soft_reset { # t => commands.t
1231 2     2 0 6 my($fileno) = @_;
1232 2         6 my $tptr = $threads->{$fileno};
1233 2         5 my $wargs = $tptr->{wargs};
1234 2         6 my $ipaddr = $tptr->{ipaddr};
1235 2   100     11 my $name = $tptr->{name} || '';
1236 2         8 $tptr = clear_bufs($fileno);
1237 2         5 $tptr->{lastc} = 'CONN';
1238 2         4 $tptr->{proto} = 'SMTP';
1239 2   33     8 $tptr->{wargs} = $wargs || '554 5.3.5 unknown mailer error'. $CRLF;
1240 2         5 $tptr->{ipaddr} = $ipaddr;
1241 2         4 $tptr->{name} = $name;
1242 2 50       8 logit(&who . $tptr->{wargs}) unless $verbose < 2;
1243 2         9 write_rearm($fileno,\&readSMTP);
1244             }
1245              
1246             sub readSMTP {
1247 0     0 0 0 my($fileno) = @_;
1248 0         0 my $tptr = $threads->{$fileno};
1249 0         0 $tptr->{alarm} = time;
1250 0         0 $tptr->{tout} = \&readRestore;
1251             }
1252              
1253             sub readRestore {
1254 0     0 0 0 my($fileno) = @_;
1255 0         0 my $tptr = $threads->{$fileno};
1256 0         0 $tptr->{read} = \&readsock;
1257 0         0 $tptr->{roff} = 0;
1258 0         0 $tptr->{next} = \&parseSMTP;
1259 0         0 $tptr->{alarm} = time + $disconnect - $delay; # five minute timeout
1260 0         0 $tptr->{tout} = \&terminate;
1261             }
1262              
1263             # return buffers to the 'ehlo,helo' state
1264             #
1265             # input: threads, fileno
1266             # returns: $threads->{$fileno}
1267             #
1268             sub clear_bufs { # t => commands.t
1269 5     5 0 38 my($fileno) = @_;
1270 5         8 my($sock,$domain,$proto) = @{$threads->{$fileno}}{qw(
  5         14  
1271             sock domain proto)};
1272 5         11 delete $threads->{$fileno}; # clean all buffers in the thread
1273 5 100       17 my $lastc = ($domain)
    100          
1274             ? ($proto eq 'ESMTP') ? 'EHLO' : 'HELO'
1275             : 'CONN';
1276 5         36 my $tptr = $threads->{$fileno} = {};
1277 5         9 @{$tptr}{qw(
  5         16  
1278             sock domain proto lastc)} = # restore only those that are needed
1279             ($sock,$domain,$proto,$lastc);
1280 5         14 return $tptr;
1281             }
1282              
1283             sub write_rearm { # t => parseSMTP.t
1284 0     0 0 0 my($fileno,$next,$immediate) = @_;
1285 0         0 my $tptr = $threads->{$fileno};
1286 0         0 $tptr->{woff} = 0;
1287 0         0 $tptr->{next} = $next;
1288 0 0       0 goto &write_delay
1289             if $immediate;
1290 0         0 $tptr->{tout} = \&write_delay;
1291 0         0 $tptr->{alarm} = time; # wait 'delay'
1292             }
1293              
1294             sub write_delay {
1295 0     0 0 0 my($fileno) = @_;
1296 0         0 my $tptr = $threads->{$fileno};
1297 0         0 $tptr->{tout} = \&terminate;
1298 0         0 $tptr->{write} = \&writesock;
1299 0         0 $tptr->{alarm} = time; # kill thread if we can't write
1300             }
1301              
1302             #=item * syslog_config();
1303             #
1304             #Configure Unix logging.
1305             #
1306             # NOTE, logging must be initiated by the caller
1307             #
1308             # input: none
1309             # output: none
1310             #
1311             #=cut
1312              
1313             sub syslog_config {
1314 0 0 0 0 0 0 if ($syslog && $syslog ne 'STDERR') {
1315 0         0 openlog($me, LOG_PID(), eval "$log_facility");
1316 0         0 $LOG = eval "$syslog"; # save LOGlevel for everyone
1317             }
1318             }
1319              
1320             #=item * logit($msg);
1321             #
1322             #Log a message.
1323             #
1324             # input: message string
1325             # output: none
1326             #
1327             #=cut
1328              
1329             sub logit {
1330 0     0 0 0 my($msg) = @_;
1331 0 0       0 return unless $syslog;
1332 0         0 $msg .= "\n";
1333 0         0 $msg =~ s/[\r\n]+/\n/g;
1334 0 0       0 if ($syslog eq 'STDERR') {
1335 0         0 print STDERR $msg;
1336             }
1337             else {
1338 0         0 syslog($LOG,"%s",$msg);
1339             }
1340             }
1341              
1342             #=item * closelog();
1343             #
1344             #Close the syslog facility if it has been opened
1345             #
1346             # input: none
1347             # returns: none
1348             #
1349             #=cut
1350              
1351             sub closelog {
1352 0     0 0 0 local $^W = 0; # no warnings;
1353 0 0 0     0 &Unix::Syslog::closelog
1354             if $syslog && $syslog ne 'STDERR';
1355             }
1356              
1357             sub get_unique { # t => uniquemsgid.t
1358 44     44 0 905878 my($seed) = @_;
1359 44 100       122 $unique = $seed if $seed;
1360 44 100       91 $unique = 1 if ++$unique > 65535;
1361 44         140 return $unique; # return an ascending number or the PID if just invoked
1362             }
1363              
1364             #=item * $msgid = uniquemsgid($seed);
1365             #
1366             #Uses 'time' as a seed (standard) unless specified. Returns an email-safe
1367             #alphanumeric string based on the time (or seed), the pid of the caller and a
1368             #random number. Guaranteed to be unique for multiple daemons with less than
1369             #65k new reqests per second.
1370             #
1371             # input: [optional seed] or [default 'time']
1372             # returns: string of the form: 'bbnPCFUDYctT'
1373             #
1374             #=cut
1375              
1376             sub uniquemsgid { # t => uniquemsgid.t
1377 30   66 30 0 2766 my $t = shift || time;
1378 30         79 my $q = sprintf("%010u",($$ << 16) + get_unique());
1379 30         49 my @serial = ();
1380 30         49 foreach(0..5) {
1381 180         181 my $x = $t % 62;
1382 180         183 my $y = $q % 62;
1383 180         308 unshift @serial,$IDarray[$x],$IDarray[$y];
1384 180         197 $t = int $t/62;
1385 180         259 $q = int $q/62;
1386             }
1387 30         137 return join('',@serial);
1388             }
1389              
1390             sub dns_send { # tested by hand
1391 0 0 0 0 0   return unless $dnsaddr && $DNSfileno; # skip if no DNS present
1392 0           my($fileno,$ipaddr) = @_;
1393 0           my $id = get_unique();
1394 0 0         logit(&who . $ipaddr ." $fileno id $id")
1395             unless $verbose < 3;
1396 0           my @ip = split(/\./,$ipaddr);
1397 0           @_ = reverse @ip;
1398 0           my $name = join('.',@_,'in-addr.arpa');
1399 0           my $buffer;
1400 0           my $offset = newhead(\$buffer,
1401             $id,
1402             BITS_QUERY | RD, # query, recursion desired
1403             1,0,0,0, # one question
1404             );
1405 0           my ($get,$put,$parse) = new Net::DNS::ToolKit::RR;
1406 0           $offset = $put->Question(\$buffer,$offset,$name,T_PTR,C_IN);
1407 0 0         return unless $buffer;
1408 0           $dns->{$id} = {
1409             fileno => $fileno,
1410             alarm => time,
1411             name => $name,
1412             };
1413 0           $threads->{$fileno}->{id} = $id; # mark original thread with this ID
1414             # UDP may not block
1415 0           send(
1416             $threads->{$DNSfileno}->{sock},
1417             $buffer,0,
1418             $dnsaddr);
1419             }
1420              
1421             sub dns_rcv { # tested by hand
1422 0     0 0   my($fileno) = @_;
1423 0           my $tptr = $threads->{$fileno};
1424 0           my $msg;
1425 0           my $sender = recv($tptr->{sock},$msg,NS_PACKETSZ,0);
1426 0 0         return undef unless $sender; # no message received
1427 0 0         return undef if length($msg) < HFIXEDSZ; # short message
1428 0           my ($off,$id,$qr,$opcode,$aa,$tc,$rd,$ra,$mbz,$ad,$cd,$rcode,
1429             $qdcount,$ancount,$nscount,$arcount)
1430             = gethead(\$msg);
1431             return undef unless
1432 0 0 0       $tc == 0 &&
      0        
      0        
      0        
      0        
      0        
1433             $qr == 1 &&
1434             $opcode == QUERY &&
1435             ($rcode == NOERROR || $rcode == NXDOMAIN || $rcode == SERVFAIL) &&
1436             $qdcount == 1 &&
1437             exists $dns->{$id};
1438 0           my $pfno = $dns->{$id}->{fileno}; # originating thread pointer
1439 0           my $pname = $dns->{$id}->{name};
1440 0           delete $dns->{$id}; # remove dns query thread
1441             return undef
1442 0 0         unless exists $threads->{$pfno};
1443 0 0         return undef unless length($msg) > HFIXEDSZ; # no message
1444 0           my ($get,$put,$parse) = new Net::DNS::ToolKit::RR;
1445 0           my($name,$t,$type,$class,$ttl,$rdl,@rdata);
1446 0           ($off,$name,$type,$class) = $get->Question(\$msg,$off);
1447 0           my $lname = lc $name;
1448 0 0 0       if ( $ancount &&
      0        
      0        
      0        
1449             $rcode == &NOERROR &&
1450             $lname eq $pname &&
1451             $type == T_PTR &&
1452             $class == C_IN
1453             ) {
1454 0           foreach(0..$ancount -1) {
1455 0           ($off,$name,$t,$class,$ttl,$rdl,@rdata) = $get->next(\$msg,$off);
1456 0 0         last if $t == T_PTR;
1457             }
1458             }
1459 0           ($name) = @rdata;
1460 0 0         if ($name) {
1461 0           $threads->{$pfno}->{name} = $name .' ';
1462 0 0         logit(&who ."$pfno rDNS $rdata[0]") unless $verbose < 3;
1463             } else {
1464 0           $threads->{$pfno}->{name} = '';
1465 0 0         logit(&who ."$pfno rDNS missing") unless $verbose < 3;
1466             }
1467 0 0         connOK($pfno) unless $threads->{$pfno}->{cok}; # log connection, continue
1468             }
1469              
1470             =head1 COPYRIGHT
1471              
1472             Copyright 2004 - 2014, Michael Robinton
1473              
1474             This program is free software; you can redistribute it and/or modify
1475             it under the terms of the GNU General Public License (except as noted
1476             otherwise in individuals sub modules) published by
1477             the Free Software Foundation; either version 2 of the License, or
1478             (at your option) any later version.
1479              
1480             This program is distributed in the hope that it will be useful,
1481             but WITHOUT ANY WARRANTY; without even the implied warranty of
1482             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1483             GNU General Public License for more details.
1484              
1485             You should have received a copy of the GNU General Public License
1486             along with this program; if not, write to the Free Software
1487             Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
1488              
1489             =head1 AUTHOR
1490              
1491             Michael Robinton
1492              
1493             =head1 SEE ALSO
1494              
1495             L on CPAN or spamcannibal.org
1496              
1497             =cut
1498              
1499             1;