wiaflos-server 14.5 KB
Newer Older
Nigel Kukard's avatar
Nigel Kukard committed
1
#!/usr/bin/perl -w
2
# Wiaflos Accounting SOAP server
3
# Copyright (C) 2009-2014, AllWorldIT
Nigel Kukard's avatar
Nigel Kukard committed
4
# Copyright (C) 2008, LinuxRulz
Nigel Kukard's avatar
Nigel Kukard committed
5
# Copyright (C) 2005-2007 Nigel Kukard  <nkukard@lbsd.net>
6
#
7 8 9 10
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
11
#
12 13 14 15
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
16
#
17 18 19
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Nigel Kukard's avatar
Nigel Kukard committed
20 21

use strict;
22
use warnings;
Nigel Kukard's avatar
Nigel Kukard committed
23 24 25


# Set library directory
Nigel Kukard's avatar
Nigel Kukard committed
26 27 28 29
use lib(
		'/usr/local/lib/wiaflos-0.0',
		'/usr/lib/wiaflos-0.0',
		'/usr/lib64/wiaflos-0.0',
Nigel Kukard's avatar
Nigel Kukard committed
30
		'awitpt/lib'
Nigel Kukard's avatar
Nigel Kukard committed
31
);
Nigel Kukard's avatar
Nigel Kukard committed
32

33
use SOAP::Transport::HTTPng;
Nigel Kukard's avatar
Nigel Kukard committed
34

35 36 37 38

# Main application
package WiaflosServer;
use base qw(SOAP::Transport::HTTPng::Daemon);
Nigel Kukard's avatar
Nigel Kukard committed
39 40

# Other stuff
Nigel Kukard's avatar
Nigel Kukard committed
41
use wiaflos::version;
42 43 44
use awitpt::db::dblayer;
use awitpt::db::dbilayer;
use awitpt::cache;
45
use wiaflos::constants;
46 47 48 49 50
use wiaflos::server::core::logging;
use wiaflos::server::core::users;
use wiaflos::server::core::templating;
use wiaflos::server::core::jobs;
use wiaflos::server::api::auth qw();
51

Nigel Kukard's avatar
Nigel Kukard committed
52 53

# Pull in HTTP transport
54 55
use Config::IniFiles;
use Getopt::Long;
56

57 58 59 60 61 62 63

# Override configuration
sub configure {
	my $self = shift;
	my $server = $self->{'server'};
	my $cfg;
	my $cmdline;
Nigel Kukard's avatar
Nigel Kukard committed
64 65


66 67 68
	# We're being called by a module, ignore
	return if (@_);

69
	# Set defaults
Nigel Kukard's avatar
Nigel Kukard committed
70
	$cfg->{'config_file'} = CONFIG_FILE;
Nigel Kukard's avatar
Nigel Kukard committed
71

72 73 74 75 76 77 78 79
	# Parse command line params
	%{$cmdline} = ();
	GetOptions(
			\%{$cmdline},
			"help",
			"config:s",
			"debug",
	);
Nigel Kukard's avatar
Nigel Kukard committed
80

81 82 83 84 85 86 87 88 89 90 91
	# Check for some args
	if ($cmdline->{'help'}) {
		$self->displayHelp();
		exit 0;
	}
	if ($cmdline->{'debug'}) {
		$server->{'log_level'} = 4;
		$cfg->{'debug'} = 1;
	}
	if (defined($cmdline->{'config'}) && $cmdline->{'config'} ne "") {
		$cfg->{'config_file'} = $cmdline->{'config'};
Nigel Kukard's avatar
Nigel Kukard committed
92 93
	}

94
	# Set defaults
95 96
	$server->{'proto'} = "tcp";
	$server->{'host'} = "*";
97 98
	$server->{'hostname'} = "localhost";
	$server->{'port'} = 1080;
99
	$server->{'timeout'} = 30;
100 101 102 103 104 105

	# Check config file exists
	if (! -f $cfg->{'config_file'}) {
		print(STDERR "ERROR: No configuration file '".$cfg->{'config_file'}."' found!\n");
		exit 1;
	}
106

107
	# Use config file, ignore case
108
	tie my %inifile, 'Config::IniFiles', (
109 110 111
			-file => $cfg->{'config_file'},
			-nocase => 1
	) or die "Failed to open config file '".$cfg->{'config_file'}."': $!";
112 113
	# Copy config
	my %config = %inifile;
114
	#untie(%inifile); # This does not support a method to destroy the data, so we ignore for now
115

116
	# Server config
117 118 119
	my @server_params = (
			'log_level','log_file',
			'syslog_logsock', 'syslog_ident', 'syslog_logopt', 'syslog_facility',
120
			'proto', 'port', 'host', 'hostname',
121
			'allow', 'deny', 'cidr_allow', 'cidr_deny',
122
			'pid_file',
123 124 125 126
			'user', 'group',
			'timeout',
	);
	foreach my $param (@server_params) {
127
		$server->{$param} = $config{'server'}{$param} if (defined($config{'server'}{$param}));
128 129 130
	}

	# Small fixup if we going to be running in background
131
	if ((my $val = $config{'server'}{'background'})) {
132
		$server->{'setsid'} = 1;
Nigel Kukard's avatar
Nigel Kukard committed
133 134
	}

135 136 137 138
	# SOAP config
	my @soap_params = (
			'plugins',
	);
139
	my $soap;
140
	foreach my $param (@soap_params) {
141
		$soap->{$param} = $config{'soap'}{$param} if (defined($config{'soap'}{$param}));
142 143 144
	}

	if (!defined($soap->{'plugins'})) {
145
		$self->log(LOG_ERR,"SOAP configuration error: 'plugins' not found");
146 147 148
		exit 1;
	}
	# Split off plugins
149 150 151
	my @soap_plugins;
	if (ref($soap->{'plugins'}) eq "ARRAY") {
		foreach my $plugin (@{$soap->{'plugins'}}) {
Nigel Kukard's avatar
Nigel Kukard committed
152
			$plugin =~ s/\s+//;
153 154 155 156 157
			push(@soap_plugins,$plugin);
		}
	} else {
		@soap_plugins = split(/\s+/,$soap->{'plugins'});
	}
158
	$soap->{'plugins'} = \@soap_plugins;
159
	# Set other stuff
160
	$soap->{'on_action'} = \&wiaflos::server::api::auth::checkAccess;
161

162 163 164
	# Template engine config
	my @template_params = (
			'path',
165
			'debug',
166 167 168 169 170 171 172 173 174 175 176 177 178 179
			'global_header',
			'global_footer',
	);
	my $templates;
	foreach my $param (@template_params) {
		$templates->{$param} = $config{'templates'}{$param} if (defined($config{'templates'}{$param}));
	}

	if (!defined($templates->{'path'})) {
		$self->log(LOG_ERR,"SOAP configuration error: Under 'templates' section, required parameter 'path' not found");
		exit 1;
	}

	# MISC config
180 181 182 183 184 185 186 187 188
	$cfg->{'hostname'} = gethostbyname($server->{'hostname'});
	$cfg->{'url'} = sprintf('%s://%s%s/',"http",$server->{'hostname'},($server->{'port'} eq "80") ? "" : ":".$server->{'port'});

	# Set server response string
	$self->{'_product_tokens'} = "Wiaflos SOAP Server v$VERSION";

	# Save our config and stuff
	$self->{'config'} = $cfg;
	$self->{'soap_config'} = $soap;
189
	$self->{'template_config'} = $templates;
190
	$self->{'cmdline'} = $cmdline;
191
	$self->{'inifile'} = \%config;
Nigel Kukard's avatar
Nigel Kukard committed
192 193 194
}


195 196 197 198 199
# Pull in plugins
sub post_configure_hook
{
	my $self = shift;
	my $soapcfg = $self->{'soap_config'};
Nigel Kukard's avatar
Nigel Kukard committed
200 201


202
	$self->log(LOG_INFO,"Initializing authentication module...");
203
	wiaflos::server::api::auth::init($self);
204
	$self->log(LOG_INFO,"Authentication module initialized.");
Nigel Kukard's avatar
Nigel Kukard committed
205

206
	$self->log(LOG_INFO,"Plugin load started...");
207 208 209 210 211

	# Load plugins
	foreach my $plugin (@{$soapcfg->{'plugins'}}) {
		# Load plugin
		my $res = eval("
212 213
			use wiaflos::server::api::$plugin;
			plugin_register(\$self,\"$plugin\",\$wiaflos::server::api::${plugin}::pluginInfo);
214 215
		");
		if ($@ || (defined($res) && $res != 0)) {
216
			$self->log(LOG_WARN,"Error loading plugin $plugin ($@)\n");
Nigel Kukard's avatar
Nigel Kukard committed
217 218 219
		}
	}

220
	$self->log(LOG_INFO,"Plugin load done, ".(@{$soapcfg->{'dispatch_to'}})." functions.");
221

222

223
	$self->log(LOG_INFO,"Initializing system modules...");
224
	wiaflos::server::core::config::Init($self);
225
	# Init templating engine
226
	wiaflos::server::core::templating::Init($self);
227
	# Init caching engine
228
	awitpt::cache::Init($self);
229
	$self->log(LOG_INFO,"System modules initialized.");
Nigel Kukard's avatar
Nigel Kukard committed
230 231
}

Nigel Kukard's avatar
Nigel Kukard committed
232

233 234 235 236
# Add function to dispatch mapping
sub map_dispatch {
	my ($self,$module,$func) = @_;
	my $soapcfg = $self->{'soap_config'};
Nigel Kukard's avatar
Nigel Kukard committed
237

238 239 240 241 242
	# Make sure class has ::'s not /'s
	my $class = $module;
	$class =~ s,/,::,g;

	push(@{$soapcfg->{'dispatch_to'}},$class."::".$func);
Nigel Kukard's avatar
Nigel Kukard committed
243 244 245
}

# Register plugin info
246 247
sub plugin_register {
	my ($self,$plugin,$info) = @_;
Nigel Kukard's avatar
Nigel Kukard committed
248 249 250 251


	# If no info, return
	if (!defined($info)) {
Nigel Kukard's avatar
Nigel Kukard committed
252
		print(STDERR "WARNING: Plugin info not found for plugin => $plugin\n");
Nigel Kukard's avatar
Nigel Kukard committed
253 254 255 256 257
		return -1;
	}

	# Set real module name & save
	$info->{'Module'} = $plugin;
258
	push(@{$self->{'soap_plugins'}},$info);
Nigel Kukard's avatar
Nigel Kukard committed
259

260 261 262 263
	# If we should, init the module
	if (defined($info->{'Init'})) {
			$info->{'Init'}($self);
	}
Nigel Kukard's avatar
Nigel Kukard committed
264 265 266

	return 0;
}
Nigel Kukard's avatar
Nigel Kukard committed
267 268


269 270 271 272 273
# Initialize child
sub child_init_hook
{
	my $self = shift;

274

275
	$self->SUPER::child_init_hook();
Nigel Kukard's avatar
Nigel Kukard committed
276 277

	# Initialize job control
278
	wiaflos::server::core::jobs::Init($self);
279 280 281
}


282 283 284 285
# Finish off child
sub child_finish_hook
{
	my $self = shift;
286

Nigel Kukard's avatar
Nigel Kukard committed
287 288 289
	$self->SUPER::child_finish_hook();

	# Shut down job control
290
	wiaflos::server::core::jobs::Finish($self);
291 292 293
}


294 295 296 297 298 299 300 301 302
# Process request in child
sub process_request
{
	my ($self) = @_;
	my $soap = $self->{'_soap_engine'};

	my $c = SOAP::Transport::HTTPng::Daemon::Client->new($self);
	my $r = $c->get_request();

303
	my $timestamp = time();
304

305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
	# Check if we got connected, if not ... bypass
	if ($self->{'client'}->{'dbh_status'} > 0) {
		# Check if we need to reconnect or not
		my $timeout = $self->{'inifile'}{'database'}{'bypass_timeout'};
		if (!defined($timeout)) {
			$self->log(LOG_ERR,"[WIAFLOS] No bypass_timeout specified for failed database connections, defaulting to 120s");
			$timeout = 120;
		}
		# Get time left
		my $timepassed = $timestamp - $self->{'client'}->{'dbh_status'};
		# Then check...
		if ($timepassed >= $timeout) {
			$self->log(LOG_NOTICE,"[WIAFLOS] Client BYPASS timeout exceeded, reconnecting...");
			exit 0;
		} else {
			$self->log(LOG_NOTICE,"[WIAFLOS] Client still in BYPASS mode, ".( $timeout - $timepassed )."s left till next reconnect");
			$self->displayMessage($c,503,"Service Unavailable","Server unavailable at this time. Please try again in 5 minutes.");
			return;
		}
	}
325
	# Setup database handle
326
	awitpt::db::dblayer::setHandle($self->{'client'}->{'dbh'});
327

328 329 330 331

	# Check what we got back from our HTTP basic authentication, anything but 0 is bad, so return
	return if ($self->getAuthStatus($c,$r));

332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
	my $response;

	# Check if we authenticated via SOAP or HTTP
	if ($r->header('soapaction')) {
		# Pull in SOAP request
		$soap->request($r);
		# Handle request
		$soap->handle;
		# Set response
		$response = $soap->response;
	# Normal HTTP ... WHY??
	} else {
		$response = $self->createResponse(<<EOF);
<p>You are currently using a normal browser to access this SOAP service.</p>
<p>Please use the appropriate SOAP client.</p>
EOF
	}
349

350
	# Nuke session
351
	wiaflos::server::api::auth::sessionUnsetData();
352
	# Nuke database handle
353
	awitpt::db::dblayer::setHandle(undef);
354

355
	# Send reply to client
356
	$c->send_response($response);
357 358
}

359
# Slightly better logging
360 361 362
sub log
{
	my ($self,$level,$msg,@args) = @_;
363 364 365 366 367 368 369

	# Check log level and set text
	my $logtxt = "UNKNOWN";
	if ($level == LOG_DEBUG) {
		$logtxt = "DEBUG";
	} elsif ($level == LOG_INFO) {
		$logtxt = "INFO";
370
	} elsif ($level == LOG_NOTICE) {
371
		$logtxt = "NOTICE";
372 373 374 375 376 377 378 379 380 381 382 383 384
	} elsif ($level == LOG_WARN) {
		$logtxt = "WARNING";
	} elsif ($level == LOG_ERR) {
		$logtxt = "ERROR";
	}

	# Parse message nicely
	if ($msg =~ /^(\[[^\]]+\]) (.*)/s) {
		$msg = "$1 $logtxt: $2";
	} else {
		$msg = "[CORE] $logtxt: $msg";
	}

385 386 387 388
	# If we have args, this is more than likely a format string & args
	if (@args > 0) {
		$msg = sprintf($msg,@args);
	}
389 390
	$self->SUPER::log($level,"[".$self->log_time." - $$] $msg",@args);
}
391 392


393 394 395 396 397
# Initialize child
sub server_exit
{
	my $self = shift;

398

399
	$self->log(LOG_INFO,"Destroying system modules...");
400
	# Destroy cache
401
	awitpt::cache::Destroy($self);
402
	$self->log(LOG_INFO,"System modules destroyed.");
403 404 405 406 407

	$self->SUPER::server_exit();
}


408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436
# Display help
sub displayHelp {
	print(STDERR<<EOF);

Usage: $0 [args]
    --config=<file>        Configuration file
    --debug                Put into debug mode

EOF
}




# Fire up server
printf(STDERR "$APPSTRING\n\n", "SOAP Server");
__PACKAGE__->run();




### MISC HTTP FUNCTIONS ###


sub getAuthStatus
{
	my ($self,$c,$r) = @_;
	my $soap = $self->{'_soap_engine'};

437

438 439 440 441 442 443 444 445
	# Pull in some variables
	my $header_host = $r->header('host');
	$header_host = $self->{'config'}->{'url'} if (!defined($header_host));
	my $peer = $self->{'server'}->{'peeraddr'};


	# Reset to requiring authentication
	my $res = -1;
446 447
	my $username = undef;
	my $password = undef;
448 449 450 451


	# Check for authentication header, if so authenticate
	if (my $authStr = $r->headers->authorization_basic()) {
452
		($username,$password) = split(/:/, $authStr, 2);
453 454
		my $dbh = $self->{'client'}->{'dbh'};

455
		my $userID = wiaflos::server::core::users::authenticate($username,$password);
456 457 458


		# Authenticate user & pull in session
459
		if ($userID > 0) {
460 461 462 463 464 465 466 467
			# Check if we authenticated via SOAP or HTTP
			if ($r->header('soapaction')) {
				$self->log(LOG_INFO,"[CORE] SOAP authenticated => User: $username, IP: $peer, User-Agent: ".$r->headers->user_agent.
						", Method: ".$r->method."\n");
			} else {
				$self->log(LOG_INFO,"[CORE] HTTP authenticated => User: $username, IP: $peer, User-Agent: ".$r->headers->user_agent.
						", Method: ".$r->method."\n");
			}
468 469

			# Get user capabilities
470
			my $capabilities = wiaflos::server::core::users::getCapabilities($userID);
471

472
			wiaflos::server::api::auth::sessionSetData(
473 474 475 476
					'Username' => $username,
					'Capabilities' => $capabilities,

					'Peer' => $peer,
477

478
					'_server' => $self,
479 480 481 482 483 484
			);
			# Successfull authentication
			$res = 0;

		# Authentication Failed
		} else {
485
			$self->log(LOG_NOTICE,"[CORE] Authentication failed => User: $username, Reason: ".wiaflos::server::core::users::Error()."");
486 487 488 489 490 491 492 493
			$res = -2;
		}
	}

	# No authentication provided
	if ($res == -1) {
		# If we not authenticated, display authentication message either via SOAP or via HTML depending
		if ($r->header('soapaction')) {
494 495
			$self->log(LOG_INFO,"[CORE] SOAP authentication request => IP: $peer, User-Agent: ".$r->headers->user_agent.
					", Method: ".$r->method."\n");
496 497 498 499 500
			$soap->request($r);
			$soap->make_fault($SOAP::Constants::FAULT_CLIENT,"Not Authenticated","No HTTP basic authentication performed\n");
			$c->send_response($soap->response);

		} else {
501 502
			$self->log(LOG_INFO,"[CORE] HTTP authentication request => IP: $peer, User-Agent: ".$r->headers->user_agent.
					", Method: ".$r->method."\n");
503 504 505
			$self->displayAuth($c);
		}

506 507

	# Failed authentication
508 509 510
	} elsif ($res == -2) {
		# If we not authenticated, display authentication message either via SOAP or via HTML depending
		if ($r->header('soapaction')) {
511 512
			$self->log(LOG_NOTICE,"[CORE] SOAP authentication failed => IP: $peer, Username: '$username', User-Agent: ".
					$r->headers->user_agent.", Method: ".$r->method."\n");
513 514 515 516 517
			$soap->request($r);
			$soap->make_fault($SOAP::Constants::FAULT_CLIENT,"Authentication Failed","Username or password invalid\n");
			$c->send_response($soap->response);

		} else {
518 519
			$self->log(LOG_NOTICE,"[CORE] HTTP Authentication failed, sending request => IP: $peer, Username: '$username', User-Agent: ".
					$r->headers->user_agent.", Method: ".$r->method."\n");
520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551
			$self->displayAuth($c);
		}
	}

	return $res;
}



# Display authentication box/page
sub displayAuth
{
	my ($self,$c) = @_;


	# Throw out message to client to authenticate first
	my $headers = HTTP::Headers->new;
	$headers->content_type("text/html");
	$headers->www_authenticate("Basic realm=\"Restricted Access\"");

	my $resp = HTTP::Response->new(
			401,"Authorization Required",
			$headers,
			<<EOF);
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
        <head>
                <title>401 Authorization Required</title>
        </head>

        <body>
                <h1>Authorization Required</h1>
552
		<p>Authorization is required to access this service.</p>
553 554 555 556 557 558 559 560 561
        </body>
</html>
EOF
	# Send response, after this the connection is closed
	$c->send_response($resp);
}



562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585
# Display a HTML page to the user
sub createResponse
{
	my ($self,$content) = @_;


	# Throw out message to client to authenticate first
	my $headers = HTTP::Headers->new;
	$headers->content_type("text/html");

	my $resp = HTTP::Response->new(
			200,"Ok",
			$headers,
			<<EOF);
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
$content
</html>
EOF

	return $resp;
}


586 587 588



Nigel Kukard's avatar
Nigel Kukard committed
589 590
1;
# vim: ts=4