Commit 2e59ea44 authored by Xavier Guimard's avatar Xavier Guimard

Replace request management in handler (#1044)

Note: this is a big change, more tests needed
parent cc1fc22d
......@@ -29,11 +29,14 @@ sub new {
$self->env->{PATH_INFO} =~ s|^$tmp|/|;
$self->{uri} = uri_unescape( $self->env->{REQUEST_URI} );
$self->{uri} =~ s|//+|/|g;
$self->{datas} = {};
$self->{error} = 0;
$self->{respHeaders} = [];
return $self;
return bless( $self, $_[0] );
}
sub datas { $_[0]->{datas} }
sub uri { $_[0]->{uri} }
sub userData {
......
......@@ -9,6 +9,7 @@ lib/Lemonldap/NG/Handler/ApacheMP2/CDA.pm
lib/Lemonldap/NG/Handler/ApacheMP2/DevOps.pm
lib/Lemonldap/NG/Handler/ApacheMP2/Main.pm
lib/Lemonldap/NG/Handler/ApacheMP2/Menu.pm
lib/Lemonldap/NG/Handler/ApacheMP2/Request.pm
lib/Lemonldap/NG/Handler/ApacheMP2/SecureToken.pm
lib/Lemonldap/NG/Handler/ApacheMP2/ServiceToken.pm
lib/Lemonldap/NG/Handler/ApacheMP2/ZimbraPreAuth.pm
......@@ -48,7 +49,6 @@ META.yml
README
t/01-Lemonldap-NG-Handler-Main.t
t/05-Lemonldap-NG-Handler-Reload.t
t/10-Lemonldap-NG-Handler-SharedConf.t
t/12-Lemonldap-NG-Handler-Jail.t
t/13-Lemonldap-NG-Handler-Fake-Safe.t
t/50-Lemonldap-NG-Handler-SecureToken.t
......
......@@ -4,6 +4,7 @@
package Lemonldap::NG::Handler::ApacheMP2;
use strict;
use Lemonldap::NG::Handler::ApacheMP2::Request;
use Lemonldap::NG::Handler::ApacheMP2::Main;
......@@ -13,30 +14,31 @@ our $VERSION = '2.0.0';
sub handler {
shift if ($#_);
my ($res) = getClass(@_)->run(@_);
return $res;
return launch( 'run', @_ );
}
sub logout {
shift if ($#_);
return getClass(@_)->unlog(@_);
return launch( 'unlog', @_ );
}
sub status {
shift if ($#_);
return getClass(@_)->getStatus(@_);
return launch( 'getStatus', @_ );
}
# Internal method to get class to load
sub getClass {
my $type = Lemonldap::NG::Handler::ApacheMP2::Main->checkType(@_);
sub launch {
my ( $sub, $r ) = @_;
my $req = Lemonldap::NG::Handler::Apache2::Request->new($r);
my $type = Lemonldap::NG::Handler::ApacheMP2::Main->checkType($req);
if ( my $t = $_[0]->dir_config('VHOSTTYPE') ) {
$type = $t;
}
my $class = "Lemonldap::NG::Handler::ApacheMP2::$type";
eval "require $class";
die $@ if ($@);
return $class;
return $class->$sub($req);
}
1;
......@@ -43,33 +43,6 @@ our $request; # Apache2::RequestRec object for current request
#*run = \&Lemonldap::NG::Handler::Main::run;
## @rmethod protected int redirectFilter(string url, Apache2::Filter f)
# Launch the current HTTP request then redirects the user to $url.
# Used by logout_app and logout_app_sso targets
# @param $url URL to redirect the user
# @param $f Current Apache2::Filter object
# @return Constant $class->OK
sub redirectFilter {
my $class = shift;
my $url = shift;
my $f = shift;
unless ( $f->ctx ) {
# Here, we can use Apache2 functions instead of set_header_out
# since this function is used only with Apache2.
$f->r->status( $class->REDIRECT );
$f->r->status_line("303 See Other");
$f->r->headers_out->unset('Location');
$f->r->err_headers_out->set( 'Location' => $url );
$f->ctx(1);
}
while ( $f->read( my $buffer, 1024 ) ) {
}
$class->updateStatus( $f->r, '$class->REDIRECT',
$class->datas->{ $class->tsv->{whatToTrace} }, 'filter' );
return $class->OK;
}
__PACKAGE__->init();
# INTERNAL METHODS
......@@ -99,36 +72,21 @@ sub setServerSignature {
};
}
sub newRequest {
my ( $class, $r ) = @_;
$request = $r;
}
## @method void set_user(string user)
# sets remote_user
# @param user string username
sub set_user {
my ( $class, $user ) = @_;
$request->user($user);
}
## @method string header_in(string header)
# returns request header value
# @param header string request header
# @return request header value
sub header_in {
my ( $class, $header ) = @_;
$header ||= $class; # to use header_in as a method or as a function
return $request->headers_in->{$header};
my ( $class, $request, $user ) = @_;
$request->env->{'psgi.r'}->user($user);
}
## @method void set_header_in(hash headers)
# sets or modifies request headers
# @param headers hash containing header names => header value
sub set_header_in {
my ( $class, %headers ) = @_;
my ( $class, $request, %headers ) = @_;
while ( my ( $h, $v ) = each %headers ) {
$request->headers_in->set( $h => $v );
$request->env->{'psgi.r'}->headers_in->set( $h => $v );
}
}
......@@ -138,11 +96,11 @@ sub set_header_in {
# header 'Auth-User' is removed, 'Auth_User' be removed also
# @param headers array with header names to remove
sub unset_header_in {
my ( $class, @headers ) = @_;
my ( $class, $request, @headers ) = @_;
foreach my $h1 (@headers) {
$h1 = lc $h1;
$h1 =~ s/-/_/g;
$request->headers_in->do(
$request->env->{'psgi.r'}->headers_in->do(
sub {
my $h = shift;
my $h2 = lc $h;
......@@ -158,120 +116,65 @@ sub unset_header_in {
# sets response headers
# @param headers hash containing header names => header value
sub set_header_out {
my ( $class, %headers ) = @_;
my ( $class, $request, %headers ) = @_;
while ( my ( $h, $v ) = each %headers ) {
$request->err_headers_out->set( $h => $v );
$request->env->{'psgi.r'}->err_headers_out->set( $h => $v );
}
}
## @method string hostname()
# returns host, as set by full URI or Host header
# @return host string Host value
sub hostname {
my $class = shift;
return $request->hostname;
}
## @method string remote_ip
# returns client IP address
# @return IP_Addr string client IP
sub remote_ip {
my $class = shift;
my $remote_ip = (
$request->connection->can('remote_ip')
? $request->connection->remote_ip
: $request->connection->client_ip
);
return $remote_ip;
}
## @method boolean is_initial_req
# returns true unless the current request is a subrequest
# @return is_initial_req boolean
sub is_initial_req {
my $class = shift;
return $request->is_initial_req;
}
## @method string args(string args)
# gets the query string
# @return args string Query string
sub args {
my $class = shift;
return $request->args();
}
## @method string uri
# returns the path portion of the URI, normalized, i.e. :
# * URL decoded (characters encoded as %XX are decoded,
# except ? in order not to merge path and query string)
# * references to relative path components "." and ".." are resolved
# * two or more adjacent slashes are merged into a single slash
# @return path portion of the URI, normalized
sub uri {
my $class = shift;
my $uri = $request->uri;
$uri =~ s#//+#/#g;
$uri =~ s#\?#%3F#g;
return $uri;
}
## @method string uri_with_args
# returns the URI, with arguments and with path portion normalized
# @return URI with normalized path portion
sub uri_with_args {
my $class = shift;
return uri . ( $request->args ? "?" . $request->args : "" );
}
## @method string unparsed_uri
# returns the full original request URI, with arguments
# @return full original request URI, with arguments
sub unparsed_uri {
my $class = shift;
return $request->unparsed_uri;
}
## @method string get_server_port
# returns the port the server is receiving the current request on
# @return port string server port
sub get_server_port {
my $class = shift;
return $request->get_server_port;
}
## @method string method
# returns the port the server is receiving the current request on
# @return port string server port
sub method {
my $class = shift;
return $request->method;
}
## Return environment variables as hash
sub env {
return \%ENV;
return $_[1]->env->{'psgi.r'}->is_initial_req;
}
## @method void print(string data)
# write data in HTTP response body
# @param data Text to add in response body
sub print {
my ( $class, $data ) = @_;
$request->print($data);
my ( $class, $request, $data ) = @_;
$request->env->{'psgi.r'}->print($data);
}
1;
__END__
## @rmethod protected int redirectFilter(string url, Apache2::Filter f)
# Launch the current HTTP request then redirects the user to $url.
# Used by logout_app and logout_app_sso targets
# @param $url URL to redirect the user
# @param $f Current Apache2::Filter object
# @return Constant $class->OK
sub redirectFilter {
my $class = shift;
my $url = shift;
my $f = shift;
unless ( $f->ctx ) {
# Here, we can use Apache2 functions instead of set_header_out
# since this function is used only with Apache2.
$f->r->status( $class->REDIRECT );
$f->r->status_line("303 See Other");
$f->r->headers_out->unset('Location');
$f->r->err_headers_out->set( 'Location' => $url );
$f->ctx(1);
}
while ( $f->read( my $buffer, 1024 ) ) {
}
$class->updateStatus( $f->r, '$class->REDIRECT',
$class->datas->{ $class->tsv->{whatToTrace} }, 'filter' );
return $class->OK;
}
## @method void addToHtmlHead(string data)
# add data at end of html head
# @param data Text to add in html head
sub addToHtmlHead {
use APR::Bucket ();
use APR::Brigade ();
my ( $class, $data ) = @_;
my ( $class, $request, $data ) = @_;
$request->add_output_filter(
sub {
my $f = shift;
......@@ -322,7 +225,7 @@ sub flatten_bb {
# add or modify parameters in POST request body
# @param $params hashref containing name => value
sub setPostParams {
my ( $class, $params ) = @_;
my ( $class, $request, $params ) = @_;
$request->add_input_filter(
sub {
my $f = shift;
......
package Lemonldap::NG::Handler::ApacheMP2::Request;
use strict;
use base 'Plack::Request';
use Plack::Util;
use URI;
use URI::Escape;
# Build Plack::Request (inspired from Plack::Handler::Apache2)
sub new {
my ( $class, $r ) = @_;
# Apache populates ENV:
$r->subprocess_env;
my $env = {
%ENV,
'psgi.version' => [ 1, 1 ],
'psgi.url_scheme' => ( $ENV{HTTPS} || 'off' ) =~ /^(?:on|1)$/i
? 'https'
: 'http',
'psgi.input' => $r,
'psgi.errors' => *STDERR,
'psgi.multithread' => Plack::Util::FALSE,
'psgi.multiprocess' => Plack::Util::TRUE,
'psgi.run_once' => Plack::Util::FALSE,
'psgi.streaming' => Plack::Util::TRUE,
'psgi.nonblocking' => Plack::Util::FALSE,
'psgix.harakiri' => Plack::Util::TRUE,
'psgix.cleanup' => Plack::Util::TRUE,
'psgix.cleanup.handlers' => [],
'psqi.r' => $r,
};
if ( defined( my $HTTP_AUTHORIZATION = $r->headers_in->{Authorization} ) ) {
$env->{HTTP_AUTHORIZATION} = $HTTP_AUTHORIZATION;
}
my $uri = URI->new( "http://" . $r->hostname . $r->{env}->{REQUEST_URI} );
$env->{PATH_INFO} = uri_unescape( $uri->path );
my $self = Plack::Request->new($env);
bless $self, $class;
return $self;
}
sub datas {
my($self) = @_;
return $self->{datas} ||= {};
}
1;
......@@ -24,8 +24,8 @@ our @EXPORT_OK = @EXPORT;
# using indefinitely a session id disclosed accidentally or maliciously.
# @return session id
sub fetchId {
my $class = shift;
if ( my $creds = $class->header_in('Authorization') ) {
my ( $class, $req ) = @_;
if ( my $creds = $req->env->{'HTTP_AUTHORIZATION'} ) {
$creds =~ s/^Basic\s+//;
my @date = localtime;
my $day = $date[5] * 366 + $date[7];
......@@ -41,17 +41,19 @@ sub fetchId {
# and if needed, ask portal to create it through a SOAP request
# @return true if the session was found, false else
sub retrieveSession {
my ( $class, $id ) = @_;
my ( $class, $req, $id ) = @_;
# First check if session already exists
if ( my $res = $class->Lemonldap::NG::Handler::Main::retrieveSession($id) )
if ( my $res =
$class->Lemonldap::NG::Handler::Main::retrieveSession( $req, $id ) )
{
return $res;
}
# Then ask portal to create it
if ( $class->createSession($id) ) {
return $class->Lemonldap::NG::Handler::Main::retrieveSession($id);
if ( $class->createSession( $req, $id ) ) {
return $class->Lemonldap::NG::Handler::Main::retrieveSession( $req,
$id );
}
else {
return 0;
......@@ -62,12 +64,12 @@ sub retrieveSession {
# Ask portal to create it through a SOAP request
# @return true if the session is created, else false
sub createSession {
my ( $class, $id ) = @_;
my ( $class, $req, $id ) = @_;
# Add client IP as X-Forwarded-For IP in SOAP request
my $xheader = $class->header_in('X-Forwarded-For');
my $xheader = $req->env->{'HTTP_X_FORWARDED_FOR'};
$xheader .= ", " if ($xheader);
$xheader .= $class->remote_ip;
$xheader .= $req->{env}->{REMOTE_ADDR};
#my $soapHeaders = HTTP::Headers->new( "X-Forwarded-For" => $xheader );
## TODO: use adminSession or sessions
......@@ -76,7 +78,7 @@ sub createSession {
# default_headers => $soapHeaders
#)->uri('urn:Lemonldap/NG/Common/PSGI/SOAPService');
my $creds = $class->header_in('Authorization');
my $creds = $req->env->{'HTTP_AUTHORIZATION'};
$creds =~ s/^Basic\s+//;
my ( $user, $pwd ) = ( decode_base64($creds) =~ /^(.*?):(.*)$/ );
$class->logger->debug("AuthBasic authentication for user: $user");
......@@ -84,18 +86,18 @@ sub createSession {
#my $soapRequest = $soapClient->getCookies( $user, $pwd, $id );
my $url = $class->tsv->{portal}->() . "/sessions/global/$id?auth";
$url =~ s#//sessions/#/sessions/#g;
my $req = HTTP::Request->new( POST => $url );
$req->header( 'X-Forwarded-For' => $xheader );
$req->header( 'Content-Type' => 'application/x-www-form-urlencoded' );
$req->header( Accept => 'application/json' );
$req->content(
my $get = HTTP::Request->new( POST => $url );
$get->header( 'X-Forwarded-For' => $xheader );
$get->header( 'Content-Type' => 'application/x-www-form-urlencoded' );
$get->header( Accept => 'application/json' );
$get->content(
build_urlencoded(
user => $user,
password => $pwd,
secret => $class->tsv->{cipher}->encrypt(time)
)
);
my $resp = $class->ua->request($req);
my $resp = $class->ua->request($get);
if ( $resp->is_success ) {
$class->userLogger->notice("Good REST authentication for $user");
......@@ -130,9 +132,9 @@ sub createSession {
## @rmethod protected void hideCookie()
# Hide user credentials to the protected application
sub hideCookie {
my $class = shift;
my ( $class, $req ) = @_;
$class->logger->debug("removing Authorization header");
$class->unset_header_in('Authorization');
$class->unset_header_in( $req, 'Authorization' );
}
## @rmethod protected int goToPortal(string url, string arg)
......@@ -142,12 +144,13 @@ sub hideCookie {
# @param $arg optionnal GET parameters
# @return Apache2::Const::REDIRECT or Apache2::Const::AUTH_REQUIRED
sub goToPortal {
my ( $class, $url, $arg ) = @_;
my ( $class, $req, $url, $arg ) = @_;
if ($arg) {
return $class->Lemonldap::NG::Handler::Main::goToPortal( $url, $arg );
return $class->Lemonldap::NG::Handler::Main::goToPortal( $req, $url,
$arg );
}
else {
$class->set_header_out(
$class->set_header_out( $req,
'WWW-Authenticate' => 'Basic realm="LemonLDAP::NG"' );
return $class->AUTH_REQUIRED;
}
......
......@@ -6,11 +6,12 @@ our $VERSION = '2.0.0';
sub run {
my ( $class, $req, $rule, $protection ) = @_;
my $uri = $class->unparsed_uri;
my $uri = $req->{env}->{REQUEST_URI};
my $cn = $class->tsv->{cookieName};
my ( $id, $session );
if ( $uri =~ s/[\?&;]${cn}cda=(\w+)$//oi ) {
if ( $id = $class->fetchId and $session = $class->retrieveSession($id) )
if ( $id = $class->fetchId($req)
and $session = $class->retrieveSession( $req, $id ) )
{
$class->logger->info(
'CDA asked for an already available session, skipping');
......@@ -19,19 +20,20 @@ sub run {
my $cdaid = $1;
$class->logger->debug("CDA request with id $cdaid");
my $cdaInfos = $class->getCDAInfos($cdaid);
my $cdaInfos = $class->getCDAInfos( $req, $cdaid );
unless ( $cdaInfos->{cookie_value} and $cdaInfos->{cookie_name} ) {
$class->logger->error("CDA request for id $cdaid is not valid");
return $class->FORBIDDEN;
}
my $redirectUrl = $class->_buildUrl($uri);
my $redirectUrl = $class->_buildUrl( $req, $uri );
my $redirectHttps = ( $redirectUrl =~ m/^https/ );
$class->set_header_out(
$req,
'Location' => $redirectUrl,
'Set-Cookie' => $cdaInfos->{cookie_name} . "=" . 'c:'
. $class->tsv->{cipher}->encrypt(
$cdaInfos->{cookie_value} . ' ' . $class->resolveAlias
$cdaInfos->{cookie_value} . ' ' . $class->resolveAlias($req)
)
. "; path=/"
. ( $redirectHttps ? "; secure" : "" )
......@@ -53,7 +55,7 @@ sub run {
# Tries to retrieve the CDA session, get infos and delete session
# @return CDA session infos
sub getCDAInfos {
my ( $class, $id ) = @_;
my ( $class, $req, $id ) = @_;
my $infos = {};
# Get the session
......
......@@ -16,33 +16,36 @@ sub ua {
}
sub grant {
my ( $class, $session, $uri, $cond, $vhost ) = @_;
$vhost ||= $class->resolveAlias;
my ( $class, $req, $session, $uri, $cond, $vhost ) = @_;
$vhost ||= $class->resolveAlias($req);
$class->tsv->{lastVhostUpdate} //= {};
unless ( $class->tsv->{defaultCondition}->{$vhost}
and ( time() - $class->tsv->{lastVhostUpdate}->{$vhost} < 600 ) )
{
$class->loadVhostConfig($vhost);
$class->loadVhostConfig( $req, $vhost );
}
return $class->Lemonldap::NG::Handler::Main::grant( $session, $uri, $cond,
$vhost );
return $class->Lemonldap::NG::Handler::Main::grant( $req, $session, $uri,
$cond, $vhost );
}
sub loadVhostConfig {
my ( $class, $vhost ) = @_;
my ( $class, $req, $vhost ) = @_;
my $json;
if ( $class->tsv->{useSafeJail} ) {
my $base = $class->localConfig->{loopBackUrl}
|| "http://127.0.0.1:" . $class->get_server_port;
my $req = HTTP::Request->new( GET => "$base/rules.json" );
$req->header( Host => $vhost );
my $resp = $class->ua->request($req);
|| "http://127.0.0.1:" . $req->{env}->{SERVER_PORT};
my $get = HTTP::Request->new( GET => "$base/rules.json" );
$get->header( Host => $vhost );
my $resp = $class->ua->request($get);
if ( $resp->is_success ) {
eval { $json = from_json( $resp->content ) };
if ($@) {
$class->logger->error(
"Bad rules.json for $vhost, skipping ($@)");
}
else {
$class->logger->info("Compiling rules.json for $vhost");
}
}
}
else {
......
......@@ -33,13 +33,13 @@ BEGIN {
sub run {
my $class = shift;
my $r = $_[0];
my $ret = $class->SUPER::run();
my $ret = $class->SUPER::run($r);
# Continue only if user is authorized
return $ret unless ( $ret == $class->OK );
# Get current URI
my $uri = Lemonldap::NG::Handler::API->uri_with_args($r);
my $uri = $r->{env}->{REQUEST_URI};
# Catch Secure Token parameters
my $localConfig = $class->localConfig;
......@@ -101,7 +101,7 @@ sub run {
return $class->_returnError( $r, $secureTokenAllowOnError ) unless $key;
# Header location
$class->set_header_in( $secureTokenHeader => $key );
$class->set_header_in( $r, $secureTokenHeader => $key );
# Remove token
eval 'use Apache2::Filter' unless ( $INC{"Apache2/Filter.pm"} );
......
......@@ -5,9 +5,9 @@ use strict;
our $VERSION = '2.0.0';
sub fetchId {
my ($class) = @_;
my $token = $class->header_in('X-Llng-Token');
return $class->Lemonldap::NG::Handler::Main::fetchId() unless ($token);
my ( $class, $req ) = @_;
my $token = $req->{env}->{HTTP_X_LLNG_TOKEN};
return $class->Lemonldap::NG::Handler::Main::fetchId($req) unless ($token);
$class->logger->debug('Found token header');
my $s = $class->tsv->{cipher}->decrypt($token);
my ( $t, $_session_id, @vhosts ) = split /:/, $s;
......@@ -19,7 +19,7 @@ sub fetchId {
$class->userLogger->warn('Expired token');
return 0;
}
my $vh = $class->resolveAlias;
my $vh = $class->resolveAlias($req);
unless ( grep { $_ eq $vh } @vhosts ) {
$class->userLogger->error(
"$vh not authorizated in token (" . join( ', ', @vhosts ) . ')' );
......
......@@ -22,7 +22,7 @@ sub run {
return $ret unless ( $ret == $class->OK );
# Get current URI
my $uri = $class->uri_with_args($req);
my $uri = $req->{env}->{REQUEST_URI};
# Get Zimbra parameters
my $localConfig = $class->localConfig;
......@@ -52,7 +52,7 @@ sub run {
# Build URL
my $zimbra_url =
$class->_buildZimbraPreAuthUrl( $zimbraPreAuthKey, $zimbraUrl,
$class->_buildZimbraPreAuthUrl( $req, $zimbraPreAuthKey, $zimbraUrl,
$class->datas->{$zimbraAccountKey},
$zimbraBy, $timeout );
......@@ -72,7 +72,7 @@ sub run {
# @param timeout Timout
# @return Zimbra PreAuth URL
sub _buildZimbraPreAuthUrl {
my ( $class, $key, $url, $account, $by, $timeout ) = @_;
my ( $class, $req, $key, $url, $account, $by, $timeout ) = @_;
# Expiration time is calculated with _utime and timeout
my $expires =
......
......@@ -4,7 +4,6 @@ use strict;
use Safe;
use Lemonldap::NG::Common::Safelib; #link protected safe Safe object
use constant SAFEWRAP => ( Safe->can("wrap_code_ref") ? 1 : 0 );
use Mouse;
has customFunctions => ( is => 'rw', isa => 'Maybe[Str]' );
......@@ -66,16 +65,10 @@ sub build_jail {
$self->jail->share_from( 'Lemonldap::NG::Common::Safelib',
$Lemonldap::NG::Common::Safelib::functions );
$self->jail->share_from(
$api,
[
<