#!/usr/bin/perl

use 5.010;
use strict;
use warnings;
use Log::Any qw($log);

# early loading to avoid target module being loaded before the patch
use Perinci::Access::Base::Patch::PeriAHS;

use File::HomeDir;
use File::Write::Rotate;
use Module::List qw(list_modules);
use Module::Load;
use Perinci::CmdLine ();
use Perinci::Gen::ForModule qw(gen_meta_for_module);
use Plack::Builder;
use Plack::Runner;

our $VERSION = '0.45'; # VERSION

our %SPEC;

$SPEC{serve} = {
    v => 1.1,
    summary => 'Serve Perl modules over HTTP(S) using Riap::HTTP protocol',
    description => <<'_',

This is a simple command-line front-end for making Perl modules accessible over
HTTP(S), using the Riap::HTTP protocol. First the specified Perl modules will be
loaded. Modules which do not contain Rinci metadata will be equipped with
metadata using Perinci::Sub::Gen::ForModule. After that, a PSGI application will
be run with the Gepok or Starman PSGI server. The PSGI application serves
requests for function calls (or other kinds of Riap request) over HTTP. Perl
modules not specified in the command-line arguments will not be accessible,
since Perinci::Access::Schemeless is used with load=>0.

Modules can be accessed using URL:

 http://HOSTNAME:PORT/api/MODULE/SUBMOD/FUNCTION?ARG1=VAL1&...

This program is mainly for testing, not recommended to be used in production,
and there are not many configuration options provided. For production, it is
recommended that you construct your own PSGI application and compose the
Plack::Middleware::PeriAHS::* middlewares directly.

_
    args => {
        modules => {
            schema => ['array*' => {
                of => 'str*',
                min_len => 1,
            }],
            req => 1,
            pos => 0,
            greedy => 1,
            summary => 'List of modules to load',
            description => <<'_',

Either specify exact module name or one using wildcard (e.g. 'Foo::Bar::*', in
which Module::List will be used to load all modules under 'Foo::Bar::').

_
        },
        riap_access_log_path => {
            schema => ['str' => {}],
            summary => 'Path for Riap request access log file',
            description => <<'_',

Default is ~/peri-htserve-riap_access.log

_
        },
        riap_access_log_size => {
            schema => ['int' => {}],
            summary => 'Maximum size for Riap request access log file',
            description => <<'_',

Default is to use File::Write::Rotate's default (10485760, a.k.a. 10MB).

If size exceeds this, file will be rotated.

_
        },
        riap_access_log_histories => {
            schema => ['int' => {}],
            summary => 'Number of old Riap request access log files to keep',
            description => <<'_',

Default is to use File::Write::Rotate's default (10).

_
        },
        server => {
            schema => ['str*' => {
                in => [qw/Starman Gepok/],
                default => 'Gepok',
            }],
            summary => 'Choose PSGI server',
            description => <<'_',

Currently only Starman or Gepok is supported. Default is Gepok.

_
        },
        starman_host => {
            schema => ['str' => {}],
            summary => 'Will be passed to Starman',
        },
        starman_port => {
            schema => ['int' => {}],
            summary => 'Will be passed to Starman',
        },
        gepok_http_ports => {
            schema => ['str' => {}],
            summary => 'Will be passed to Gepok',
        },
        gepok_https_ports => {
            schema => ['str' => {}],
            summary => 'Will be passed to Gepok',
        },
        gepok_unix_sockets => {
            schema => ['str' => {}],
            summary => 'Will be passed to Gepok',
        },
        gepok_ssl_key_file => {
            schema => ['str' => {}],
            summary => 'Will be passed to Gepok',
        },
        gepok_ssl_cert_file => {
            schema => ['str' => {}],
            summary => 'Will be passed to Gepok',
        },
        daemonize => {
            schema => ['bool' => {
                default => 0,
            }],
            summary => 'If true, will daemonize into background',
            cmdline_aliases => {D=>{}},
        },
        library => {
            schema => ['array' => {
                of => 'str*',
            }],
            summary => 'Add directory to library search path, a la Perl\'s -I',
            description => <<'_',

Note that some modules are already loaded before this option takes effect. To
make sure some directories are processed, you can use `PERL5OPT` or explicitly
use `perl` and use its `-I` option.

_
            cmdline_aliases => {I=>{}},
        },

        parse_form => {
            schema => ['bool'],
            summary => 'Passed to Plack::Middleware::PeriAHS::ParseRequest',
        },
        parse_reform => {
            schema => ['bool'],
            summary => 'Passed to Plack::Middleware::PeriAHS::ParseRequest',
        },
        parse_path_info => {
            schema => ['bool'],
            summary => 'Passed to Plack::Middleware::PeriAHS::ParseRequest',
        },
        user => {
            schema => ['str*'],
            summary => 'Protect with HTTP authentication, specify username',
        },
        password => {
            schema => ['str*'],
            summary => 'Protect with HTTP authentication, specify password',
        },
        enable_logging => {
            schema  => ['bool', default=>1],
            summary => 'Can be used to test server with no support for logging',
        },
    },
    #'_perinci.sub.wrapper.validate_args' => 0,
};
sub serve {
    my %args = @_; # VXALIDATE_ARGS

    my $server = $args{server};
    #$log->tracef("TMP: modules: %s", $args{modules});
    $log->infof("Starting server (using %s) ...", $server);

    my $riap_access_log_path = $args{riap_access_log_path} //
        File::HomeDir->my_home . "/peri-htserve-riap_access.log";

    for my $dir (@{ $args{library} // [] }) {
        require lib;
        lib->import($dir);
    }

    my @modules;
    for my $m (@{$args{modules}}) {
        if ($m =~ /(.+::)\*$/) {
            my $res = list_modules($1, {list_modules=>1});
            push @modules, keys %$res;
        } else {
            push @modules, $m;
        }
    }
    $log->debugf("Modules to load: %s", \@modules);
    for my $m (@modules) {
        $log->infof("Loading module %s ...", $m);
        eval { load $m };
        return [500, "Failed to load module $m: $@"] if $@;
        gen_meta_for_module(module=>$m, load=>0);
    }

    my $fwr;
    {
        my ($dir, $leaf) = $riap_access_log_path =~ m!(.+)/(.+)!;
        if (!$dir) { $dir = "."; $leaf = $riap_access_log_path }
        $fwr = File::Write::Rotate->new(
            dir       => $dir,
            prefix    => $leaf,
            size      => $args{riap_access_log_size},
            histories => $args{riap_access_log_histories},
        );
    }

    # let's only allow access to perl modules (and not other schemes like http).
    # let's not dynamically load modules except the ones explicitly specified
    # and loaded above. let's only allow seeing the specified modules.
    require Perinci::Access::Schemeless;
    my $pa = Perinci::Access::Schemeless->new(
        load => 0,
        allow_paths => [map {s!::!/!g; "/$_"} @modules],
    );

    my $app = builder {
        enable(
            "PeriAHS::LogAccess",
            dest => $fwr,
        );

        #enable "PeriAHS::CheckAccess";

        if (defined($args{user}) && defined($args{password})) {
            enable(
                "Auth::Basic",
                authenticator => sub {
                    my ($user, $pass, $env) = @_;

                    if ($user eq $args{user} && $pass eq $args{password}) {
                        #$env->{"REMOTE_USER"} = $user; # isn't this already done by webserver?
                        return 1;
                    }
                    return 0;
                }
            );
        }

        enable(
            "PeriAHS::ParseRequest",
            parse_path_info => $args{parse_path_info},
            parse_form      => $args{parse_form},
            parse_reform    => $args{parse_reform},
            riap_client     => $pa,
        );

        enable (
            "PeriAHS::Respond",
            enable_logging => $args{enable_logging},
        );
    };

    my @argv;
    push @argv, "-s", $server;
    if ($server eq 'Starman') {
        for (qw/host port/) {
            push @argv, "--$_", $args{"starman_$_"} if $args{"starman_$_"};
        }
    } else {
        if (!$args{gepok_http_ports} &&
                !$args{gepok_https_ports} &&
                    !$args{gepok_unix_sockets}) {
            $args{gepok_http_ports} = "*:5000";
        }
        for (qw/http_port https_ports unix_sockets
                ssl_key_file ssl_cert_file/) {
            push @argv, "--$_", $args{"gepok_$_"} if $args{"gepok_$_"};
        }
    }
    push @argv, "-D" if $args{daemonize};
    my $runner = Plack::Runner->new;
    $runner->parse_options(@argv);
    $runner->run($app);

    # never reached though
    [200, "OK"];
}

Perinci::CmdLine->new(url => '/main/serve')->run;

#ABSTRACT: Serve Perl modules over HTTP(S) using the Riap::HTTP protocol
#PODNAME: peri-htserve

__END__

=pod

=encoding UTF-8

=head1 NAME

peri-htserve - Serve Perl modules over HTTP(S) using the Riap::HTTP protocol

=head1 VERSION

This document describes version 0.45 of peri-htserve (from Perl distribution Perinci-Access-HTTP-Server), released on 2014-06-11.

=head1 SYNOPSIS

 # serve modules over HTTP, using default options (HTTP port 5000)
 $ peri-htserve Foo::Bar Baz::*

 # you can now do
 $ curl 'http://localhost:5000/api/Baz/SubMod/func1?arg1=1&arg2=2'
 [200,"OK",{"The":"result","...":"..."}]

 # or use the Perl client
 $ perl -MPerinci::Access -e'
     my $pa = Perinci::Access->new;
     my $res = $pa->request(call=>"http://localhost:5000/api/Foo/Bar/func2");'


 ### some other peri-htserve options:

 # change ports/etc (see http_ports, https_ports, and unix_sockets in Gepok doc)
 $ peri-htserve --http-ports "localhost:5000,*:80" ...

 # see all available options
 $ peri-htserve --help

=head1 DESCRIPTION

For now, please see source code for more details (or --help).

=head1 QUICK TIPS

=head2 Complex argument

In raw HTTP, you can send complex argument by encoding it in JSON, e.g.:

 $ curl 'http://localhost:5000/api/Foo/Bar/func?array:j=[1,2,3]'

Notice the ":j" suffix after parameter name.

=head1 TODO

=over 4

=item * Pass more Plackup options.

=item * Pass more PSGI server options.

=back

=head1 SEE ALSO

L<Riap::HTTP>

L<Perinci::Access>, L<Perinci::Access::HTTP::Client>

PSGI servers used: L<Gepok>, L<Starman>

L<Plack::Runner>

=head1 HOMEPAGE

Please visit the project's homepage at L<https://metacpan.org/release/Perinci-Access-HTTP-Server>.

=head1 SOURCE

Source repository is at L<https://github.com/sharyanto/perl-Perinci-Access-HTTP-Server>.

=head1 BUGS

Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Perinci-Access-HTTP-Server>

When submitting a bug or request, please include a test-file or a
patch to an existing test-file that illustrates the bug or desired
feature.

=head1 AUTHOR

Steven Haryanto <stevenharyanto@gmail.com>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2014 by Steven Haryanto.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
