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