Subversion Repositories general

Rev

Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
1082 dev 1
#!/usr/bin/perl -w
2
 
3
#
4
# Sample backend for HostAdmiral
5
# (copyleft) Anatoli Klassen
6
#
7
 
8
# FIXME use transactions
9
 
10
use strict;
11
use vars;
12
use subs;
13
use Socket;
14
use DBI;
15
use Time::HiRes qw( gettimeofday tv_interval );
16
 
17
# == configuration =============================
18
 
19
my $host        = '127.0.0.1';
20
my $port        = 9097;
21
my $password    = '0123456789ABCDEF';
22
my $db_url      = 'DBI:mysql:database=mail;host=localhost;port=3306';
23
my $db_user     = 'root';
24
my $db_password = '';
25
my $log_level   = 9;                              # 0 - none, 9 - all
26
 
27
# == constants =================================
28
 
29
my $protocol_ver_maj   = "1";
30
my $protocol_ver_min   = "0";
31
my $protocol_header    = "HostAdmiral_TcpListener";
32
my $password_header    = "password=";
33
my $domain_header      = "inetDomain";
34
my $user_header        = "user";
35
my $system_user_header = "systemUser";
36
my $mailbox_header     = "mailbox";
37
my $mail_alias_header  = "mailAlias";
38
my $create_action      = "create";
39
my $modify_action      = "modify";
40
my $delete_action      = "delete";
41
 
42
# response codes
43
my $code_ok               = 200;
44
my $code_ok_but           = 201;
45
my $code_ignored          = 202;
46
my $code_no_body          = 400;
47
my $code_protocol_header  = 401;
48
my $code_no_end_lines     = 402;
49
my $code_no_password      = 403;
50
my $code_wrong_password   = 404;
51
my $code_no_command       = 405;
52
my $code_wrong_command    = 406;
53
my $code_unknown_command  = 407;
54
my $code_wrong_params     = 408;
55
my $code_db_connect_error = 501;
56
my $code_db_error         = 502;
57
my $code_db_close_error   = 503;
58
 
59
# == internal global variables =================
60
 
61
my %handlers;
62
 
63
sub connection_loop
64
{
65
	# listen for connections
66
	socket(SERVER, PF_INET, SOCK_STREAM, getprotobyname('tcp')) or die "$!\n";
67
	setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR, 1)             or die "$!\n";
68
	bind(SERVER, sockaddr_in($port, inet_aton($host)))          or die "$!\n";
69
	listen(SERVER, 1);
70
 
71
	while(1) {
72
		# get connection
73
		my $rem_addr = accept(CLIENT, SERVER);
74
		my $buf;
75
		my $body = "";
76
		my %request = ();
77
		#log_debug("Remote: $rem_addr");
78
		$request{'start_timestamp'} = [gettimeofday];
79
 
80
		# receive request body
81
		while((my $size = sysread CLIENT, $buf, 65536) > 0) {
82
			$body .= $buf;
83
		}
84
		$request{'body'} = $body;
85
		$request{'body_timestamp'} = [gettimeofday];
86
 
87
		# call handler
88
		handle_request(\%request);
89
		$request{'done_timestamp'} = [gettimeofday];
90
 
91
		# print out response
92
		print CLIENT "$protocol_header $protocol_ver_maj.$protocol_ver_min"
93
			. "\n$request{'code'} $request{'response'}\n\n";
94
		close CLIENT;
95
		$request{'stop_timestamp'} = [gettimeofday];
96
		log_debug("Duration: " . tv_interval($request{'start_timestamp'}, 
97
			$request{'stop_timestamp'}) . " sec");
98
	}
99
 
100
	# close the port
101
	close SERVER;
102
}
103
 
104
sub handle_request
105
{
106
	my $request = shift @_;
107
 
108
	log_debug("Handle request [\n$request->{'body'}]");
109
 
110
	my @lines = split /\n/, $request->{'body'}, -1;
111
	my $cur   = 0;
112
 
113
	# check header
114
	if($#lines < $cur) {
115
		set_request_code($request, $code_no_body, "Request without body");
116
		return;
117
	}
118
	unless($lines[$cur] =~ /^$protocol_header $protocol_ver_maj\.\d+$/) {
119
		set_request_code($request, $code_protocol_header, "Request must start"
120
			. "with [$protocol_header $protocol_ver_maj.minor],"
121
			. " but [$lines[$cur]] found");
122
		return;
123
	}
124
	$cur++;
125
 
126
	# check end lines
127
	if($#lines < $cur+1 || $lines[$#lines-1] ne "" || $lines[$#lines] ne "") {
128
		set_request_code($request, $code_no_end_lines, 
129
			"Request doesn't end with \\n\\n");
130
		return;
131
	}
132
 
133
	# check password
134
	if($password) {
135
		if($#lines < $cur || !($lines[$cur] =~ /^$password_header/)) {
136
			set_request_code($request, $code_no_password, 
137
				"Password not specified");
138
			return;
139
		}
140
 
141
		unless($lines[$cur] =~ /^$password_header$password$/) {
142
			set_request_code($request, $code_wrong_password,
143
				"Wrong password");
144
			return;
145
		}
146
 
147
		$cur++;
148
	}
149
 
150
	# get command handler
151
	if($#lines < $cur) {
152
		set_request_code($request, $code_no_command, "Empty command");
153
		return;
154
	}
155
	unless($lines[$cur] =~ /^(\S+)\t(\S+)/) {
156
		set_request_code($request, $code_wrong_command, "Can not get command");
157
		return;
158
	}
159
 
160
	$request->{'command'}    = $1;
161
	$request->{'subcommand'} = $2;
162
	$request->{'handler'}    = $handlers{"$1_$2"};
163
 
164
	unless($request->{'handler'}) {
165
		set_request_code($request, $code_unknown_command,
166
			"Unknown command [$request->{'command'} $request->{'subcommand'}]");
167
		return;
168
	}
169
 
170
	# call
171
	log_debug("call $request->{'command'}_$request->{'subcommand'}");
172
	my @params = @lines[$cur..$#lines-2];
173
	&{$request->{'handler'}}($request, @params);
174
}
175
 
176
sub handle_user_create
177
{
178
	my $request = shift @_;
179
 
180
	set_request_code($request, $code_ignored, "Not interesting in users");
181
}
182
 
183
sub handle_user_modify
184
{
185
	my $request = shift @_;
186
 
187
	set_request_code($request, $code_ignored, "Not interesting in users");
188
}
189
 
190
sub handle_user_delete
191
{
192
	my $request = shift @_;
193
 
194
	set_request_code($request, $code_ignored, "Not interesting in users");
195
}
196
 
197
sub handle_domain_create
198
{
199
	my $request = shift @_;
200
	my %params  = parse_command_params($request, shift @_, ("name"));
201
	return unless(%params);
202
 
203
	my $res_action = save_to_db($request, "transport", 
204
		{ domain => $params{"name"} },
205
		{ domain => $params{"name"}, comment => $params{"comment"},
206
		  transport => 'virtual:' } );
207
 
208
	if($res_action eq 'update') {
209
		return unless(restart_mail_system());
210
		set_request_code($request, $code_ok_but, "Domain exists, modified");
211
	}
212
	elsif($res_action eq 'insert') {
213
		return unless(restart_mail_system());
214
		set_request_code($request, $code_ok, "Domain created");
215
	}
216
}
217
 
218
sub handle_domain_modify
219
{
220
	my $request = shift @_;
221
	my %params  = parse_command_params($request, shift @_, ("oldName", "name"));
222
	return unless(%params);
223
 
224
	my $res_action = save_to_db($request, "transport", 
225
		{ domain => $params{"oldName"} },
226
		{ domain => $params{"name"}, comment => $params{"comment"},
227
		  transport => 'virtual:' } );
228
 
229
	if($res_action eq 'update') {
230
		return unless(restart_mail_system());
231
		set_request_code($request, $code_ok, "Domain modified");
232
	}
233
	elsif($res_action eq 'insert') {
234
		return unless(restart_mail_system());
235
		set_request_code($request, $code_ok_but, "Domain not found, created");
236
	}
237
}
238
 
239
sub handle_domain_delete
240
{
241
	my $request = shift @_;
242
	my %params  = parse_command_params($request, shift @_, ("name"));
243
	return unless(%params);
244
 
245
	my $res_action = delete_from_db($request, "transport", 
246
		{ domain => $params{"name"} } );
247
 
248
	if($res_action eq 'delete') {
249
		return unless(restart_mail_system());
250
		set_request_code($request, $code_ok, "Domain deleted");
251
	}
252
	elsif($res_action eq 'not found') {
253
		set_request_code($request, $code_ok_but, "Domain not found");
254
	}
255
}
256
 
257
sub handle_system_user_create
258
{
259
}
260
 
261
sub handle_system_user_modify
262
{
263
}
264
 
265
sub handle_system_user_delete
266
{
267
}
268
 
269
sub handle_mailbox_create
270
{
271
	my $request = shift @_;
272
	my %params  = parse_command_params($request, shift @_,
273
		("login", "password", "domain"));
274
	return unless(%params);
275
 
276
	my $res_action = save_to_db($request, "users", 
277
		{ login => "$params{'login'}\@$params{'domain'}" },
278
		{ login => "$params{'login'}\@$params{'domain'}",
279
		  password => $params{"password"},
280
		  maildir => "$params{'domain'}/$params{'login'}/",
281
		  expired => ($params{"enabled"} eq "true" ? 0 : 1),
282
		  comment => $params{"comment"},
283
		  uid => ($params{"systemUser"} ? $params{"systemUser"} : undef) } );
284
 
285
	if($res_action eq 'update') {
286
		set_request_code($request, $code_ok_but, "Mailbox exists, modified");
287
	}
288
	elsif($res_action eq 'insert') {
289
		set_request_code($request, $code_ok, "Mailbox created");
290
	}
291
}
292
 
293
sub handle_mailbox_modify
294
{
295
	my $request = shift @_;
296
	my %params  = parse_command_params($request, shift @_,
297
		("oldLogin", "oldDomain", "login", "domain"));
298
	return unless(%params);
299
 
300
	# FIXME move the old maildir
301
 
302
	my $res_action = save_to_db($request, "users", 
303
		{ login => "$params{'oldLogin'}\@$params{'oldDomain'}" },
304
		{ login => "$params{'login'}\@$params{'domain'}",
305
		  password => $params{"password"},
306
		  maildir => "$params{'domain'}/$params{'login'}/",
307
		  expired => ($params{"enabled"} eq "true" ? "0" : "1"),
308
		  comment => $params{"comment"},
309
		  uid => ($params{"systemUser"} ? $params{"systemUser"} : undef) } );
310
 
311
	if($res_action eq 'update') {
312
		set_request_code($request, $code_ok, "Mailbox modified");
313
	}
314
	elsif($res_action eq 'insert') {
315
		set_request_code($request, $code_ok_but, "Mailbox not found, created");
316
	}
317
}
318
 
319
sub handle_mailbox_delete
320
{
321
	my $request = shift @_;
322
	my %params  = parse_command_params($request, shift @_, ("login", "domain"));
323
	return unless(%params);
324
 
325
	# FIXME remove the maildir
326
 
327
	my $res_action = delete_from_db($request, "users", 
328
		{ login => "$params{'login'}\@$params{'domain'}" } );
329
 
330
	if($res_action eq 'delete') {
331
		set_request_code($request, $code_ok, "Mailbox deleted");
332
	}
333
	elsif($res_action eq 'not found') {
334
		set_request_code($request, $code_ok_but, "Mailbox not found");
335
	}
336
}
337
 
338
sub handle_mail_alias_create
339
{
340
	my $request = shift @_;
341
	my %params  = parse_command_params($request, shift @_, ("address", "domain"));
342
	return unless(%params);
343
	my @rcpts = parse_command_array($request, @_);
344
 
345
	my $del_action = delete_from_db($request, "aliases", 
346
		{ alias => "$params{'address'}\@$params{'domain'}" } );
347
	return if($del_action eq "error");
348
 
349
	foreach my $rcpt (@rcpts) {
350
		log_debug("save $rcpt");
351
		my $res_action = save_to_db($request, "aliases", 
352
			undef,
353
			{ alias => "$params{'address'}\@$params{'domain'}",
354
			  rcpt => $rcpt, comment => $params{"comment"} } );
355
		return if($res_action eq "error");
356
	}
357
 
358
	if($del_action eq 'delete') {
359
		set_request_code($request, $code_ok_but, "Mail alias exists, modified");
360
	}
361
	elsif($del_action eq 'not found') {
362
		set_request_code($request, $code_ok, "Mail alias created");
363
	}
364
}
365
 
366
sub handle_mail_alias_modify
367
{
368
	my $request = shift @_;
369
	my %params  = parse_command_params($request, shift @_, ("address", "domain"));
370
	return unless(%params);
371
	my @rcpts = parse_command_array($request, @_);
372
 
373
	my $del_action = delete_from_db($request, "aliases", 
374
		{ alias => "$params{'address'}\@$params{'domain'}" } );
375
	return if($del_action eq "error");
376
 
377
	foreach my $rcpt (@rcpts) {
378
		log_debug("save $rcpt");
379
		my $res_action = save_to_db($request, "aliases", 
380
			undef,
381
			{ alias => "$params{'address'}\@$params{'domain'}",
382
			  rcpt => $rcpt, comment => $params{"comment"} } );
383
		return if($res_action eq "error");
384
	}
385
 
386
	if($del_action eq 'delete') {
387
		set_request_code($request, $code_ok, "Mail alias modified");
388
	}
389
	elsif($del_action eq 'not found') {
390
		set_request_code($request, $code_ok_but, "Mail alias not found, created");
391
	}
392
}
393
 
394
sub handle_mail_alias_delete
395
{
396
	my $request = shift @_;
397
	my %params  = parse_command_params($request, shift @_, ("address", "domain"));
398
	return unless(%params);
399
 
400
	my $res_action = delete_from_db($request, "aliases", 
401
		{ alias => "$params{'address'}\@$params{'domain'}" } );
402
 
403
	if($res_action eq 'delete') {
404
		set_request_code($request, $code_ok, "Mail alias deleted");
405
	}
406
	elsif($res_action eq 'not found') {
407
		set_request_code($request, $code_ok_but, "Mail alias not found");
408
	}
409
}
410
 
411
sub decode_param
412
{
413
	my $value = shift @_;
414
 
415
	$value =~ s/\\r/\r/g;
416
	$value =~ s/\\n/\n/g;
417
	$value =~ s/\\t/\t/g;
418
	$value =~ s/\\0/\000/g;
419
	$value =~ s/\\\\/\\/g;
420
 
421
	return $value;
422
}
423
 
424
sub parse_command_array
425
{
426
	my $request = shift @_;
427
	my @params  = ();
428
 
429
	map {
430
		if(/^\t(.*)$/) {
431
			push @params, decode_param($1);
432
		}
433
	} @_;
434
 
435
	return @params;
436
}
437
 
438
sub parse_command_params($$@)
439
{
440
	my $request  = shift @_;
441
	my @params   = split /\t/, shift @_, -1;
442
	my %required = map { $_ => 1 } @_; # convert array to hash
443
	my %values   = ();
444
 
445
	@params = @params[2..$#params]; # remove handler and action
446
	map {
447
		my ($key, $value) = split /=/, $_;
448
		$values{$key} = decode_param($value);
449
		delete($required{$key});
450
	} @params;
451
 
452
	if(%required) {
453
		set_request_code($request, $code_wrong_params,
454
			"Params " . join(', ', keys %required) . " expected but not found");
455
		return ();
456
	}
457
 
458
	return %values;
459
}
460
 
461
sub restart_mail_system
462
{
463
	my $request = shift @_;
464
 
465
	log_debug("Mail system restarted");
466
 
467
	return 1;
468
}
469
 
470
sub db_connect
471
{
472
	my $request = shift @_;
473
	my $dbh     = undef;
474
 
475
	eval { $dbh = DBI->connect($db_url, $db_user, $db_password); };
476
	if($@) {
477
		set_request_code($request, $code_db_connect_error, $@);
478
		$dbh = undef;
479
	}
480
 
481
	return $dbh;
482
}
483
 
484
sub db_close
485
{
486
	my $request  = shift @_;
487
	my $dbh      = shift @_;
488
	my $error    = shift @_;
489
	my $no_error = 1;
490
 
491
	if($error) {
492
		set_request_code($request, $code_db_error, $error);
493
		$no_error = 0;
494
	}
495
 
496
	eval {
497
		$dbh->disconnect() if($dbh);
498
	};
499
 
500
	if($@ && $no_error) {
501
		set_request_code($request, $code_db_close_error, $@);
502
		$no_error = 0;
503
	}
504
 
505
	return $no_error;
506
}
507
 
508
sub delete_from_db
509
{
510
	my $request     = shift @_;
511
	my $table       = shift @_;
512
	my $key_columns = shift @_;
513
 
514
	my $res_action  = 'none';
515
	my $dbh         = db_connect($request);
516
 
517
	return 'error' unless($dbh);
518
 
519
	eval {
520
		my $sql = "";
521
		while(my ($key, $value) = each(%$key_columns)) {
522
			next unless(defined $value);
523
			$sql .= " and " if($sql);
524
			$sql .= "$key = ?";
525
		}
526
		$sql = "delete from $table where $sql";
527
 
528
		my $sth   = $dbh->prepare($sql);
529
		my $count = 0;
530
		while(my ($key, $value) = each(%$key_columns)) {
531
			next unless(defined $value);
532
			$sth->bind_param(++$count, $value);
533
		}
534
 
535
		my $res = $sth->execute;
536
 
537
		if($res < 1) {
538
			$res_action = 'not found';
539
		}
540
		else {
541
			$res_action = 'delete';
542
		}
543
	};
544
 
545
	if(db_close($request, $dbh, $@)) {
546
		return $res_action;
547
	}
548
	else {
549
		return 'error';
550
	}
551
}
552
 
553
sub save_to_db
554
{
555
	my $request       = shift @_;
556
	my $table         = shift @_;
557
	my $key_columns   = shift @_;
558
	my $value_columns = shift @_;
559
 
560
	my $error_set  = 0;
561
	my $res_action = 'none';
562
	my $dbh        = db_connect($request);
563
 
564
	return 'error' unless($dbh);
565
 
566
	eval {
567
		my $res = 0;
568
 
569
		if($key_columns) {
570
			my $update_sql = "";
571
			my $where      = "";
572
			while(my ($key, $value) = each(%$value_columns)) {
573
				next unless(defined $value);
574
				$update_sql .= ", " if($update_sql);
575
				$update_sql .= "$key = ?";
576
			}
577
			while(my ($key, $value) = each(%$key_columns)) {
578
				next unless(defined $value);
579
				$where .= " and " if($where);
580
				$where .= "$key = ?";
581
			}
582
			$update_sql = "update $table set $update_sql where $where";
583
 
584
			my $update_sth = $dbh->prepare($update_sql);
585
			my $count      = 0;
586
			while(my ($key, $value) = each(%$value_columns)) {
587
				next unless(defined $value);
588
				$update_sth->bind_param(++$count, $value);
589
			}
590
			while(my ($key, $value) = each(%$key_columns)) {
591
				next unless(defined $value);
592
				$update_sth->bind_param(++$count, $value);
593
			}
594
 
595
			$res        = $update_sth->execute;
596
			$res_action = 'update';
597
		}
598
 
599
		if($res < 1) {
600
			my $insert_sql = "";
601
			my $sql_params = "";
602
			while(my ($key, $value) = each(%$key_columns)) {
603
				next unless(defined $value);
604
				next if($value_columns->{$key});
605
				if($insert_sql) {
606
					$insert_sql .= ", ";
607
					$sql_params .= ", ";
608
				}
609
				$insert_sql .= "$key";
610
				$sql_params .= "?";
611
			}
612
			while(my ($key, $value) = each(%$value_columns)) {
613
				next unless(defined $value);
614
				if($insert_sql) {
615
					$insert_sql .= ", ";
616
					$sql_params .= ", ";
617
				}
618
				$insert_sql .= "$key";
619
				$sql_params .= "?";
620
			}
621
			$insert_sql = "insert into $table ($insert_sql)"
622
				. " values ($sql_params)";
623
 
624
			my $insert_sth = $dbh->prepare($insert_sql);
625
			my $count      = 0;
626
			while(my ($key, $value) = each(%$key_columns)) {
627
				next unless(defined $value);
628
				next if($value_columns->{$key});
629
				$insert_sth->bind_param(++$count, $value);
630
			}
631
			while(my ($key, $value) = each(%$value_columns)) {
632
				next unless(defined $value);
633
				$insert_sth->bind_param(++$count, $value);
634
			}
635
 
636
			$res        = $insert_sth->execute;
637
			$res_action = 'insert';
638
		}
639
	};
640
 
641
	if(db_close($request, $dbh, $@)) {
642
		return $res_action;
643
	}
644
	else {
645
		return 'error';
646
	}
647
}
648
 
649
sub set_request_code
650
{
651
	my $request = shift @_;
652
	my $code    = shift @_;
653
	my $message = shift @_;
654
 
655
	$request->{'code'}     = $code;
656
	$request->{'response'} = $message;
657
 
658
	my $error = "Error $code '$message' in request [\n$request->{'body'}]";
659
	if($code >= 500 && $code < 600) {
660
		log_error($error);
661
	}
662
	elsif($code >= 400 && $code < 500) {
663
		log_warning($error);
664
	}
665
	elsif($code >= 200 && $code < 300) {
666
		log_info("$code $message");
667
	}
668
	else {
669
		log_error("unknown code $code");
670
	}
671
}
672
 
673
sub log_debug
674
{
675
	log_message("DEBUG", shift @_) if ($log_level >= 9);
676
}
677
 
678
sub log_info
679
{
680
	log_message("INFO", shift @_) if ($log_level >= 5);
681
}
682
 
683
sub log_warning
684
{
685
	log_message("WARN", shift @_) if ($log_level >= 3);
686
}
687
 
688
sub log_error
689
{
690
	log_message("ERROR", shift @_) if ($log_level >= 1);
691
}
692
 
693
sub log_message
694
{
695
	print shift @_, ":\t", shift @_, "\n";
696
}
697
 
698
sub init
699
{
700
	$handlers{"${user_header}_${create_action}"}        = \&handle_user_create;
701
	$handlers{"${user_header}_${modify_action}"}        = \&handle_user_modify;
702
	$handlers{"${user_header}_${delete_action}"}        = \&handle_user_delete;
703
	$handlers{"${domain_header}_${create_action}"}      = \&handle_domain_create;
704
	$handlers{"${domain_header}_${modify_action}"}      = \&handle_domain_modify;
705
	$handlers{"${domain_header}_${delete_action}"}      = \&handle_domain_delete;
706
	$handlers{"${system_user_header}_${create_action}"} = \&handle_system_user_create;
707
	$handlers{"${system_user_header}_${modify_action}"} = \&handle_system_user_modify;
708
	$handlers{"${system_user_header}_${delete_action}"} = \&handle_system_user_delete;
709
	$handlers{"${mailbox_header}_${create_action}"}     = \&handle_mailbox_create;
710
	$handlers{"${mailbox_header}_${modify_action}"}     = \&handle_mailbox_modify;
711
	$handlers{"${mailbox_header}_${delete_action}"}     = \&handle_mailbox_delete;
712
	$handlers{"${mail_alias_header}_${create_action}"}  = \&handle_mail_alias_create;
713
	$handlers{"${mail_alias_header}_${modify_action}"}  = \&handle_mail_alias_modify;
714
	$handlers{"${mail_alias_header}_${delete_action}"}  = \&handle_mail_alias_delete;
715
}
716
 
717
sub main
718
{
719
	#my $sth = $dbh->prepare("SELECT * FROM transport");
720
	#$sth->execute();
721
	#while(my $ref = $sth->fetchrow_hashref()) {
722
	#	print "id = $ref->{'id'}\n";
723
	#}
724
	#$sth->finish();
725
 
726
	init();
727
	connection_loop();
728
}
729
 
730
main();
731