1package OpenXPKI::Client::Config; 2 3use Moose; 4use File::Spec; 5use Cache::LRU; 6use Config::Std; 7use Data::Dumper; 8use OpenXPKI::Client; 9use OpenXPKI::Log4perl; 10use OpenXPKI::i18n qw( set_language set_locale_prefix); 11 12=head1 OpenXPKI::Client::Config 13 14This is a helper package for all cgi based client interfaces to read a 15client config file base on the name of the script called. It was designed 16to work inside an apache server but should do in other environments as 17long as the environment variables are available or adjusted. 18 19=head2 Environment variables 20 21=over 22 23=item OPENXPKI_CLIENT_CONF_DIR 24 25The path of the OpenXPKI base config directory, default I</usr/local/etc/openxpki> 26 27=item OPENXPKI_I<SERVICE>_CLIENT_CONF_DIR 28 29I<SERVICE> is the name of the service as given to the constructor. 30The default value is the basedir plus the name of the service, e.g. 31I</usr/local/etc/openxpki/scep>. This is the base directory where the service 32config is initialized from the file I<default.conf>. If you use 33the config autodiscovery feature (config name from script name), those 34files need to be here, too. 35 36Note: Dashes in the servicename are replaced by underscores, e.g. the 37name for I<scep-test> is I<OPENXPKI_SCEP_TEST_CLIENT_CONF_DIR>. 38 39It is B<not> used if an expicit config file is set with 40OPENXPKI_I<SERVICE>_CLIENT_CONF_FILE! 41 42=item OPENXPKI_I<SERVICE>_CLIENT_CONF_FILE 43 44The full path of the config file to use. 45 46=item OPENXPKI_CLIENT_SERVICE_NAME 47 48The name of the service. 49B<Note> This overrides the service name passed to the constructor! 50 51=back 52 53=head2 Default Configuration 54 55Mostly logger config, used before FCGI is spawned and if no special 56config is found. 57 58=head2 Entity Configuration / Autodiscovery 59 60Most cgi wrappers offer autodiscovery of config files based on the 61scripts filename, which is espacially handy with rewrite or alias rules. 62E.g. with the default scep configuration you can use 63http://servername/scep/my-endpoint in your scep client which will load 64the entity configuration from the file I<my-endpoint.conf> in the scep 65config directory (by default /usr/local/etc/openxpki/scep, see also notes above). 66 67If no such file is found, the default configuration is used. 68 69=head2 Isntance Variables / Accessor Methods 70 71=head3 service 72 73Name of the service as passed during construction, read-only 74 75=cut 76 77has 'service' => ( 78 required => 1, 79 is => 'ro', 80 isa => 'Str', 81); 82 83=head3 basepath 84 85The filesystem path holding the config directories, can be set during 86construction, defaults to I</usr/local/etc/openxpki> when not read from ENV 87(see above). 88 89=cut 90 91# the service specific path 92has 'basepath' => ( 93 required => 0, 94 is => 'ro', 95 isa => 'Str', 96 lazy => 1, 97 builder => '__init_basepath' 98); 99 100=head3 logger 101 102The Log4perl instance. Will be created from the global section of the 103config file read but can also be set. 104 105=cut 106 107has 'logger' => ( 108 required => 0, 109 lazy => 1, 110 is => 'rw', 111 isa => 'Object', 112 builder => '__init_logger', 113); 114 115=head3 default 116 117Accessor to the default configuration, usually read from I<default.conf>. 118 119=cut 120 121has 'default' => ( 122 required => 0, 123 is => 'rw', 124 isa => 'HashRef', 125 lazy => 1, 126 builder => '__init_default', 127); 128 129=head3 endpoint 130 131Name of the endpoint that is used for config discovery, set from the 132script name when C<parse_uri> is called. Can also be set explicit. 133 134=cut 135 136has 'endpoint' => ( 137 required => 0, 138 is => 'rw', 139 isa => 'Str|Undef', 140 lazy => 1, 141 default => '', 142); 143 144=head3 route 145 146The name of the route extracted from the script name by C<parse_uri>. 147 148=cut 149 150has 'route' => ( 151 required => 0, 152 is => 'rw', 153 isa => 'Str', 154 lazy => 1, 155 default => '', 156); 157 158=head3 language 159 160The name of the current language, set whenever a config is loaded that has 161the language propery set. Sets the gettext path when changed. 162 163=cut 164 165has language => ( 166 required => 0, 167 is => 'rw', 168 isa => 'Str', 169 lazy => 1, 170 default => '', 171 trigger => sub { 172 my $self = shift; 173 set_language($self->language()); 174 }, 175); 176 177=head3 client 178 179Instance of OpenXPKI::Client, autogenerated with the default socket 180path if not set. 181 182=cut 183 184has 'client' => ( 185 required => 0, 186 is => 'rw', 187 isa => 'OpenXPKI::Client', 188 lazy => 1, 189 predicate => "has_client", 190 default => sub { 191 return OpenXPKI::Client->new( socketfile => '/var/openxpki/openxpki.socket' ); 192 } 193); 194 195has '_cache' => ( 196 required => 0, 197 is => 'ro', 198 isa => 'Cache::LRU', 199 lazy => 1, 200 default => sub { 201 return Cache::LRU->new( size => 16 ); 202 } 203); 204 205# this allows a constructor with the service as scalar 206around BUILDARGS => sub { 207 208 my $orig = shift; 209 my $class = shift; 210 211 my $args = shift; 212 if (!ref $args) { 213 $args = { service => $args }; 214 } 215 216 # try to read service name from ENV 217 if ($ENV{OPENXPKI_CLIENT_SERVICE_NAME}) { 218 $args->{service} = $ENV{OPENXPKI_CLIENT_SERVICE_NAME}; 219 } 220 221 return $class->$orig( $args ); 222 223}; 224 225sub BUILD { 226 227 my $self = shift; 228 229 if ($self->service() !~ /\A[a-zA-Z0-9\-]+\z/) { 230 die "Invalid service name: " . $self->service(); 231 } 232 233 my $config = $self->default(); 234 235 if ($config->{global}->{locale_directory}) { 236 set_locale_prefix($config->{global}->{locale_directory}); 237 } 238 if ($config->{global}->{default_language}) { 239 $self->language($config->{global}->{default_language}); 240 } 241 242 $self->logger()->debug(sprintf('Config for service %s loaded', $self->service())); 243 $self->logger()->trace('Global config: ' . Dumper $config ) if $self->logger->is_trace; 244 245} 246 247sub __init_basepath { 248 249 my $self = shift; 250 251 # generate name of the environemnt values from the service name 252 my $env_dir = 'OPENXPKI_'.uc($self->service()).'_CLIENT_CONF_DIR'; 253 $env_dir =~ s{-}{_}g; 254 255 # check for service specific basedir in env 256 if ( $ENV{$env_dir} ) { 257 -d $ENV{$env_dir} 258 || die sprintf "Explicit config directory not found (%s, from env %s)", $ENV{$env_dir}, $env_dir; 259 260 return File::Spec->canonpath( $ENV{$env_dir} ); 261 } 262 263 my $path; 264 # check for a customized global base dir 265 if ($ENV{OPENXPKI_CLIENT_CONF_DIR}) { 266 $path = $ENV{OPENXPKI_CLIENT_CONF_DIR}; 267 if (!-d $path) { 268 die "Explicit client config path does not exists! ($path)"; 269 } 270 $path = File::Spec->canonpath( $path ); 271 } else { 272 $path = '/usr/local/etc/openxpki'; 273 } 274 275 # default basedir is global path + servicename 276 return File::Spec->catdir( ( $path, $self->service() ) ); 277 278} 279 280sub __init_default { 281 282 my $self = shift; 283 # in case an explicit script name is set, we do NOT use the default.conf 284 my $service = $self->service(); 285 my $env_file = 'OPENXPKI_'.uc($service).'_CLIENT_CONF_FILE'; 286 my $env_socket = 'OPENXPKI_'.uc($service).'_CLIENT_CONF_SOCKET'; 287 288 my $configfile; 289 if (my $conf_socket = $ENV{$env_socket}) { 290 my $client = OpenXPKI::Client->new( socketfile => $conf_socket ); 291 $self->client($client); 292 my $reply = $client->send_receive_service_msg('GET_ENDPOINT_CONFIG', { 'interface' => $service }); 293 die "Unable to fetch endpoint default configuration from backend" unless (ref $reply->{PARAMS}); 294 return $reply->{PARAMS}->{CONFIG}; 295 296 } elsif ($ENV{$env_file}) { 297 -f $ENV{$env_file} 298 || die sprintf "Explicit config file not found (%s, from env %s)", $ENV{$env_file}, $env_file; 299 300 $configfile = $ENV{$env_file}; 301 302 } else { 303 $configfile = File::Spec->catfile( ( ($self->basepath), 'default.conf' ) ); 304 } 305 306 my $config; 307 if (!read_config $configfile => $config) { 308 die "Could not read client config file " . $configfile; 309 } 310 311 # cast to an unblessed hash 312 my %config = %{$config}; 313 return \%config; 314 315} 316 317=head2 Methods 318 319=head3 parse_uri 320 321Try to parse endpoint and route based on the script url in the 322environment. Always returns $self, endpoint is set to the empty 323string if parsing fails. 324 325=cut 326 327sub parse_uri() { 328 329 my $self = shift; 330 331 # generate name of the environemnt values from the service name 332 my $service = $self->service(); 333 334 $self->endpoint(''); 335 $self->route(''); 336 337 # Test for specific config file based on script name 338 # SCRIPT_URL is only available with mod_rewrite 339 # expected pattern is servicename/endpoint/route, 340 # route can contain a suffix like .exe which is used by some scep clients 341 my ($ep, $rt); 342 if (defined $ENV{SCRIPT_URL}) { 343 ($ep, $rt) = $ENV{SCRIPT_URL} =~ qr@ ${service} / ([^/]+) (?:/ ([\w\-\/]+ (?:\.\w{3})? ) )?\z@x; 344 } elsif (defined $ENV{REQUEST_URI}) { 345 ($ep,$rt) = $ENV{REQUEST_URI} =~ qr@ ${service} / ([^/\?]+) (?:/([\w\-\/]+ (?:\.\w{3})? ))? (\?.*)? \z@x; 346 } 347 348 if (!$ep) { 349 $self->logger()->warn("Unable to detect script name - please check the docs"); 350 $self->logger()->trace(Dumper \%ENV) if $self->logger->is_debug; 351 } elsif (($service =~ m{(est|cmc)}) && !$rt) { 352 $self->logger()->debug("URI without endpoint, setting route: $ep"); 353 $self->endpoint('default'); 354 $self->route($ep); 355 } else { 356 $self->endpoint($ep); 357 $self->route($rt) if ($rt); 358 $self->logger()->debug("Parsed URI: $ep => ".($rt||'')); 359 } 360 361 return $self; 362 363} 364 365=head3 config 366 367Returns the config hashref for the current endpoint. 368 369=cut 370 371sub config() { 372 373 my $self = shift; 374 my $config; 375 my $cacheid = $self->endpoint() || 'default'; 376 if (!($config = $self->_cache()->get( $cacheid ))) { 377 # non existing files and other errors are handled inside loader 378 $config = $self->__load_config(); 379 $self->_cache()->set( $cacheid => $config ); 380 $self->logger()->debug('added config to cache ' . $cacheid); 381 } 382 383 $self->language($config->{global}->{default_language} || $self->default()->{global}->{default_language} || ''); 384 385 return $config; 386 387} 388 389sub __load_config { 390 391 my $self = shift; 392 393 my $file; 394 my $config; 395 if ($self->endpoint()) { 396 # config via socket 397 if ($self->has_client()) { 398 $self->logger()->debug('Autodetect config for service ' . $self->service() . ' via socket '); 399 my $reply = $self->client()->send_receive_service_msg('GET_ENDPOINT_CONFIG', 400 { 'interface' => $self->service(), endpoint => $self->endpoint() }); 401 die "Unable to fetch endpoint default configuration from backend" unless (ref $reply->{PARAMS}); 402 return $reply->{PARAMS}->{CONFIG}; 403 } 404 $file = $self->endpoint().'.conf'; 405 } 406 407 if ($file) { 408 $self->logger()->debug('Autodetect config file for service ' . $self->service() . ': ' . $file ); 409 $file = File::Spec->catfile( ($self->basepath() ), $file ); 410 if (! -f $file ) { 411 $self->logger()->debug('No config file found, falling back to default'); 412 $file = undef; 413 } 414 } 415 416 # if no config file is given, use the default 417 return $self->default() unless($file); 418 419 if (!read_config $file => $config) { 420 $self->logger()->error('Unable to read config from file ' . $file); 421 die "Could not read client config file $file "; 422 } 423 424 # cast to an unblessed hash 425 my %config = %{$config}; 426 427 $self->logger()->trace('Script config: ' . Dumper \%config ) if $self->logger->is_trace; 428 429 return \%config; 430} 431 432sub __init_logger { 433 my $self = shift; 434 my $config = $self->default(); 435 436 OpenXPKI::Log4perl->init_or_fallback( $config->{global}->{log_config} ); 437 438 return Log::Log4perl->get_logger($config->{global}->{log_facility} || ''); 439} 440 4411; 442 443__END__; 444