mirror of
https://github.com/ovh/the-bastion.git
synced 2025-01-11 01:41:39 +08:00
500 lines
22 KiB
Perl
500 lines
22 KiB
Perl
package OVH::Bastion::ProxyHTTP;
|
|
|
|
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
|
|
use common::sense;
|
|
|
|
use File::Basename;
|
|
use lib dirname(__FILE__) . '/../../../../lib/perl';
|
|
use OVH::Result;
|
|
use OVH::Bastion;
|
|
|
|
use CGI;
|
|
use Fcntl qw(:flock);
|
|
use Time::HiRes ();
|
|
use MIME::Base64;
|
|
use Net::Server::PreForkSimple;
|
|
use Net::Server::PreFork;
|
|
use Sys::Hostname;
|
|
use base qw{Net::Server::HTTP};
|
|
|
|
###########################
|
|
# BE CAREFUL IN THIS CLASS: STDIN && STDOUT are bound to the server->client socket
|
|
# of the current request, in other words, everything that is printed to stdout goes
|
|
# to the network, this is NOT the case with stderr
|
|
|
|
# override Net::Server::HTTP::send_status, because it hardcodes content-type: text/html,
|
|
# and there's no way for the caller to prevent that :(
|
|
sub send_status {
|
|
my ($self, $status, $msg, $body) = @_;
|
|
$msg ||= ($status == 200) ? 'OK' : '-';
|
|
my $request_info = $self->{'request_info'};
|
|
|
|
my $content_type_already_sent = 0;
|
|
my $want_gzip = 0;
|
|
my $out = "HTTP/1.0 $status $msg\r\n";
|
|
foreach my $row (@{$self->http_base_headers}) {
|
|
$out .= "$row->[0]: $row->[1]\r\n";
|
|
push @{$request_info->{'response_headers'}}, $row;
|
|
$content_type_already_sent++ if (lc($row->[0]) eq 'content-type');
|
|
$want_gzip++ if (lc($row->[0]) eq 'content-encoding' && $row->[1] =~ /gzip/);
|
|
}
|
|
$self->{'server'}->{'client'}->print($out);
|
|
$request_info->{'http_version'} = '1.0';
|
|
$request_info->{'response_status'} = $status;
|
|
$request_info->{'response_header_size'} += length $out;
|
|
|
|
if ($body) {
|
|
|
|
# add Content-Type only if not already defined
|
|
if (not $content_type_already_sent) {
|
|
push @{$request_info->{'response_headers'}}, ['Content-type', 'text/plain'];
|
|
$out = "Content-Type: text/plain\r\n\r\n";
|
|
}
|
|
else {
|
|
$out = "\r\n";
|
|
}
|
|
$request_info->{'response_header_size'} += length $out;
|
|
$self->{'server'}->{'client'}->print($out);
|
|
$request_info->{'headers_sent'} = 1;
|
|
my $encoded_body = $body;
|
|
if ($want_gzip) {
|
|
require IO::Compress::Gzip;
|
|
IO::Compress::Gzip::gzip(\$body => \$encoded_body);
|
|
}
|
|
$self->{'server'}->{'client'}->print($encoded_body);
|
|
$request_info->{'response_size'} += length $encoded_body;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub log_and_exit {
|
|
my ($self, $code, $msg, $body, $params) = @_;
|
|
$params->{'returnvalue'} ||= "$code $msg";
|
|
|
|
my $account = delete $self->{'_log'}{'account'};
|
|
my $user = delete $self->{'_log'}{'user'};
|
|
my $hostto = delete $self->{'_log'}{'hostto'};
|
|
my $portto = delete $self->{'_log'}{'portto'};
|
|
my $starttime = delete $self->{'_log'}{'start_time'};
|
|
my $allowed = delete $self->{'_log'}{'allowed'} || 0;
|
|
my $bastion2device_delay = delete $self->{'_log'}{'bastion2device_delay'};
|
|
my $request_body_length = delete $self->{'_log'}{'request_body_length'};
|
|
|
|
# log in sql and/or logfile and/or syslog
|
|
my $processing_delay = ($starttime ? int(Time::HiRes::tv_interval($starttime) * 1_000_000) : undef);
|
|
$params->{'account'} = $account; # might be undef if we're called before the account is extracted from the payload
|
|
$params->{'user'} = $user; # ditto
|
|
$params->{'hostto'} = $hostto; # ditto
|
|
$params->{'portto'} = $portto; # ditto
|
|
$params->{'loghome'} = 'proxyhttp';
|
|
$params->{'cmdtype'} = 'proxyhttp_daemon';
|
|
$params->{'ipfrom'} = $self->{'request_info'}{'peeraddr'};
|
|
$params->{'portfrom'} = $self->{'request_info'}{'peerport'};
|
|
$params->{'bastionip'} = $self->{'request_info'}{'sockaddr'};
|
|
$params->{'bastionport'} = $self->{'request_info'}{'sockport'};
|
|
$params->{'params'} = $self->{'request_info'}{'request_path'};
|
|
$params->{'plugin'} = uc($self->{'request_info'}{'request_method'});
|
|
$params->{'allowed'} = $allowed;
|
|
|
|
# custom data will only be logged to logfile and syslog, not sql (it's not in the generic schema)
|
|
$params->{'custom'} = [
|
|
['user_agent' => $ENV{'HTTP_USER_AGENT'}],
|
|
['request_headers_length' => $self->{'request_info'}{'request_header_size'}],
|
|
['request_body_length' => $request_body_length + 0],
|
|
['response_body_length' => length($body)],
|
|
['timing_bastion2device_usec' => $bastion2device_delay],
|
|
['timing_global_usec' => $processing_delay],
|
|
['code' => $code],
|
|
['msg' => $msg],
|
|
];
|
|
if ($processing_delay) {
|
|
push @{$params->{'custom'}}, ['timing_overhead_usec' => ($processing_delay - $bastion2device_delay) + 0];
|
|
}
|
|
$self->{'_log'}{'logret'} = OVH::Bastion::log_access_insert(%$params);
|
|
|
|
# log in "ttyrec"
|
|
my $basedir = "/home/proxyhttp/ttyrec";
|
|
-d $basedir || mkdir $basedir;
|
|
|
|
my $srcip = 'src_' . ($ENV{'REMOTE_ADDR'} || '0.0.0.0');
|
|
my $finaldir = "$basedir/$srcip";
|
|
-d $finaldir || mkdir $finaldir;
|
|
|
|
my @now = Time::HiRes::gettimeofday();
|
|
my @t = localtime($now[0]);
|
|
|
|
my @request_lines = ($self->{'request_info'}{'request'});
|
|
foreach my $array (@{$self->{'request_info'}{'request_headers'} || []}) {
|
|
push @request_lines, sprintf("%s: %s", @$array);
|
|
}
|
|
if (exists $self->{'_log'}{'post_content'}) {
|
|
push @request_lines, '';
|
|
push @request_lines, delete $self->{'_log'}{'post_content'};
|
|
}
|
|
|
|
my $logfile = sprintf("%s/%s.txt", $finaldir, POSIX::strftime("%F", @t));
|
|
my $logline = sprintf(
|
|
"--- CLIENT_REQUEST UNIQID=%s TIMESTAMP=%d.%06d DATE=%s ---\n%s\n"
|
|
. "--- BASTION_ANSWER UNIQID=%s TIMESTAMP=%d.%06d DATE=%s ---\n%s\n"
|
|
. "--- END UNIQID=%s TIMESTAMP=%d.%06d DATE=%s ---\n\n",
|
|
$ENV{'UNIQID'}, $now[0], $now[1],
|
|
POSIX::strftime("%Y/%m/%d.%H:%M:%S", @t),
|
|
join("\n", @request_lines),
|
|
$ENV{'UNIQID'}, $now[0], $now[1],
|
|
POSIX::strftime("%Y/%m/%d.%H:%M:%S", @t),
|
|
"HTTP/1.0 $code $msg\n\n$body",
|
|
$ENV{'UNIQID'}, $now[0], $now[1], POSIX::strftime("%Y/%m/%d.%H:%M:%S", @t),
|
|
);
|
|
$logline =~ s/^(Authorization:).+/$1 (removed)/mgi;
|
|
|
|
if (open(my $log, '>>', $logfile)) {
|
|
flock($log, LOCK_EX);
|
|
print $log $logline;
|
|
flock($log, LOCK_UN);
|
|
close($log);
|
|
}
|
|
else {
|
|
warn("Couldn't open $logfile for log write");
|
|
}
|
|
|
|
# if status is 401, tell client what scheme we expect for the auth
|
|
if ($code == 401) {
|
|
push @{$self->{'_supplementary_headers'}}, ['WWW-Authenticate', 'Basic realm="bastion"'];
|
|
}
|
|
|
|
# and send status (will also fills access_log)
|
|
return $self->send_status($code, $msg, $body . "\n");
|
|
}
|
|
|
|
# called by Net::Server when initializing, we set its log_function here to handle error logs, such as timeouts.
|
|
# if func is undefined when Net::Server needs it, it'll log to STDERR, and we don't want that
|
|
sub configure_hook { ## no critic (RequireFinalReturn)
|
|
my $self = shift;
|
|
$self->{'server'}{'log_function'} = sub {
|
|
my ($level, $msg) = @_;
|
|
warn_syslog("osh-http-proxy-daemon: level $level: $msg");
|
|
}
|
|
}
|
|
|
|
# overrides parent func
|
|
sub run {
|
|
my ($self, %params) = @_;
|
|
$self->{'proxy_config'} = (delete $params{'proxy_config'}) || {};
|
|
return $self->SUPER::run($self, %params);
|
|
}
|
|
|
|
# overrides parent func
|
|
sub process_http_request {
|
|
my ($self, $client) = @_;
|
|
my $fnret;
|
|
|
|
$ENV{'FORCE_STDERR'} = 1;
|
|
$self->{'_log'}{'start_time'} = [Time::HiRes::gettimeofday()];
|
|
|
|
$ENV{'UNIQID'} = OVH::Bastion::generate_uniq_id()->value;
|
|
|
|
$self->{'server'}{'access_log_format'} = qq#%h - - %t "%r" %>s %b "-" "$ENV{'UNIQID'}" "%{User-Agent}i" %D -#;
|
|
|
|
# consistency check
|
|
if ($self->{'request_info'}{'peerport'} ne $ENV{'REMOTE_PORT'}) {
|
|
return $self->log_and_exit(
|
|
500,
|
|
"Internal Server Error (consistency)",
|
|
"Internal consistency error: remote_port doesn't match peerport, this shouldn't happen",
|
|
{comment => 'consistency_error'}
|
|
);
|
|
}
|
|
|
|
# only GET or POST are allowed
|
|
if (not grep { uc($self->{'request_info'}{'request_method'}) eq $_ } qw{ GET POST }) {
|
|
return $self->log_and_exit(400, "Bad Request (method forbidden)", "Only GET and POST methods are allowed", {comment => 'method_forbidden'});
|
|
}
|
|
|
|
# if we don't have the request_headers, we really have a big problem
|
|
if (ref $self->{'request_info'} ne 'HASH' or ref $self->{'request_info'}{'request_headers'} ne 'ARRAY') {
|
|
return $self->log_and_exit(500, "Internal Server Error (headers not found)", "The headers of the request can't be found", {comment => "headers_not_found"});
|
|
}
|
|
|
|
# convert headers into a hash
|
|
my $req_headers = _flatten_headers($self->{'request_info'}{'request_headers'});
|
|
if (ref $req_headers ne 'HASH') {
|
|
return $self->log_and_exit(500, "Internal Server Error (headers are not a hash)", "Request headers couldn't be parsed properly", {comment => "headers_cannot_be_parsed"});
|
|
}
|
|
|
|
# check if it's not just a self-health test
|
|
if ($ENV{'REQUEST_URI'} eq '/bastion-health-check') {
|
|
|
|
# launch the worker in monitoring mode, to be sure we test all the sudo part
|
|
my @cmd = ("sudo", "-n", "-u", "proxyhttp", "--", "/usr/bin/env", "perl", "-T", "/opt/bastion/bin/proxy/osh-http-proxy-worker");
|
|
push @cmd, "--monitoring", "--uniqid", $ENV{'UNIQID'};
|
|
|
|
$fnret = OVH::Bastion::execute(cmd => \@cmd, noisy_stdout => 0, noisy_stderr => 1, is_helper => 1);
|
|
$fnret
|
|
or return $self->log_and_exit(500, "Internal Error (couldn't call helper)", "Couldn't call helper (" . $fnret->msg . ")", {comment => "exec1_failed"});
|
|
|
|
$fnret = OVH::Bastion::result_from_helper($fnret->value->{'stdout'});
|
|
$fnret
|
|
or return $self->log_and_exit(500, "Internal Error (helper execution failed)", "Helper execution failed (" . $fnret->msg . ")", {comment => "exec2_failed"});
|
|
|
|
$fnret = OVH::Bastion::helper_decapsulate($fnret->value);
|
|
$fnret
|
|
or return $self->log_and_exit(500, "Internal Error (helper returned an error)", "Helper returned an error (" . $fnret->msg . ")", {comment => "exec3_failed"});
|
|
|
|
my $workerversion = $fnret->value->{'body'};
|
|
|
|
if ($workerversion eq $OVH::Bastion::VERSION) {
|
|
return $self->log_and_exit(200, "OK", "Bastion HTTPS proxy version " . $OVH::Bastion::VERSION . " is running nominally.", {comment => "monitoring"});
|
|
}
|
|
else {
|
|
return $self->log_and_exit(
|
|
202,
|
|
"Semi-OK",
|
|
"A discrepancy was found between the Bastion HTTPS proxy daemon version ("
|
|
. $OVH::Bastion::VERSION
|
|
. ") and worker version ($workerversion), please reload the daemon to avoid problems.",
|
|
{comment => "monitoring"}
|
|
);
|
|
}
|
|
}
|
|
|
|
# this header is mandatory, and must be a Basic scheme auth
|
|
if (not $req_headers->{'authorization'}) {
|
|
return $self->log_and_exit(401, "Authorization required (no auth provided)", "No authentication provided, and authentication is mandatory",
|
|
{comment => "no_auth_provided"});
|
|
}
|
|
my $basic_auth_header_value;
|
|
if (not $req_headers->{'authorization'} =~ m{^Basic (\S+)$}i) {
|
|
return $self->log_and_exit(401, "Authorization required (basic auth scheme needed)", "Basic authorization scheme required", {comment => "bad_auth_scheme"});
|
|
}
|
|
else {
|
|
$basic_auth_header_value = $1;
|
|
}
|
|
if ($req_headers->{'authorization'} ne $ENV{'HTTP_AUTHORIZATION'}) {
|
|
return $self->log_and_exit(500, "Internal Server Error (consistency)", "Internal consistency error: authorization header doesn't match envvar",
|
|
{comment => 'consistency_error'});
|
|
}
|
|
delete $ENV{'HTTP_AUTHORIZATION'};
|
|
|
|
# decode the auth header
|
|
my $decoded = decode_base64($basic_auth_header_value);
|
|
undef $basic_auth_header_value;
|
|
|
|
#print STDERR "I decoded $decoded\n";
|
|
|
|
# the decoded header should be of the form LOGIN:PASSWORD
|
|
if (not $decoded or $decoded !~ /^(.+):([^:]+)$/) {
|
|
return $self->log_and_exit(401, "Authorization required (malformed basic auth)", "Malformed Basic authorization '$decoded'", {comment => "malformed_basic_auth"});
|
|
}
|
|
|
|
# in our case, the LOGIN should in fact be of the form bastion_account@remote_login_expression@remote_host_to_connect_to,
|
|
# where remote_login_expression can be one of "$user", "group=$shortGroup,user=$user" or "user=$user"
|
|
my ($loginpart, $pass) = ($1, $2); ## no critic (ProhibitCaptureWithoutTest)
|
|
if ($loginpart !~ m{^([^@]+)@([^@]+)@([0-9a-zA-Z._-]+)(%(\d+))?$}) { ## no critic (ProhibitUnusedCapture)
|
|
return $self->log_and_exit(
|
|
400,
|
|
"Bad Request (bad login format)",
|
|
"Expected an Authorization line with credentials of the form 'BASTIONACCOUNT\@DEVICEUSER\@HOST' or "
|
|
. "'BASTIONACCOUNT\@group=BASTIONGROUP,user=DEVICEUSER\@HOST' or 'BASTIONACCOUNT\@user=DEVICEUSER\@HOST'",
|
|
{comment => "bad_login_format"}
|
|
);
|
|
}
|
|
my ($account, $user_expression, $remotemachine, $remoteport) = ($1, $2, $3, $5); ## no critic (ProhibitCaptureWithoutTest)
|
|
undef $loginpart; # no longer needed
|
|
$remoteport = 443 if not defined $remoteport;
|
|
$self->{'_log'}{'hostto'} = $remotemachine;
|
|
$self->{'_log'}{'portto'} = $remoteport;
|
|
|
|
my $context;
|
|
my $group;
|
|
my $user;
|
|
if ($user_expression =~ m{^group=(\S+),user=(\S+)$}) {
|
|
$context = 'group';
|
|
$group = $1;
|
|
$user = $2;
|
|
}
|
|
elsif ($user_expression =~ m{^user=(\S+),group=(\S+)$}) {
|
|
$context = 'group';
|
|
$group = $2;
|
|
$user = $1;
|
|
}
|
|
elsif ($user_expression =~ m{^user=(\S+)$}) {
|
|
$context = 'self';
|
|
$user = $1;
|
|
}
|
|
else {
|
|
$context = 'autodetect';
|
|
$user = $user_expression;
|
|
}
|
|
undef $user_expression; # no longer needed
|
|
|
|
if (not OVH::Bastion::is_account_valid(account => $account)) {
|
|
return $self->log_and_exit(400, "Bad Request (bad account)", "Account name is invalid", {comment => "invalid_account"});
|
|
}
|
|
my $escaped_account = $account;
|
|
$escaped_account =~ s/%/%%/g;
|
|
$self->{'server'}{'access_log_format'} = qq#%h $escaped_account %u %t "%r" %>s %b "$remotemachine" "$ENV{'UNIQID'}" "%{User-Agent}i" %D -#;
|
|
if (not OVH::Bastion::is_valid_port(port => $remoteport)) {
|
|
return $self->log_and_exit(400, "Bad Request (bad port number)", "Port number is out of range", {comment => "invalid_port_number"});
|
|
}
|
|
|
|
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
|
|
if (not $fnret) {
|
|
|
|
# don't be too specific on the error message to avoid account name guessing
|
|
return $self->log_and_exit(403, "Access Denied", "Incorrect username ($account) or password (#REDACTED#, length=" . length($pass) . ")",
|
|
{comment => "invalid_credentials"});
|
|
}
|
|
$account = $fnret->value->{'account'}; # untaint
|
|
$self->{'_log'}{'account'} = $account;
|
|
|
|
if ($user !~ /^[a-zA-Z0-9._-]+/) {
|
|
return $self->log_and_exit(400, "Bad Request (bad user name)", "User name '$user' has forbidden characters", {comment => "bad_user_name"});
|
|
}
|
|
$self->{'_log'}{'user'} = $user;
|
|
my $escaped_user = $user;
|
|
$escaped_user =~ s/%/%%/g;
|
|
$self->{'server'}{'access_log_format'} = qq#%h $escaped_account $escaped_user %t "%r" %>s %b "$remotemachine" "$ENV{'UNIQID'}" "%{User-Agent}i" %D -#;
|
|
|
|
# config value by default
|
|
my $timeout = $self->{'proxy_config'}{'timeout'};
|
|
|
|
# if there's a timeout header, get it
|
|
if ($req_headers->{'x-bastion-timeout'}) {
|
|
if ($req_headers->{'x-bastion-timeout'} =~ /^\d+$/) {
|
|
$timeout = $req_headers->{'x-bastion-timeout'};
|
|
}
|
|
else {
|
|
return $self->log_and_exit(400, "Bad Request (invalid timeout value)", "Expected an integer timeout value expressed in seconds", {comment => "bad_timeout_value"});
|
|
}
|
|
}
|
|
|
|
# if there's an allow-downgrade header, get it
|
|
my $allow_downgrade = 0;
|
|
if ($req_headers->{'x-bastion-allow-downgrade'}) {
|
|
if ($req_headers->{'x-bastion-allow-downgrade'} eq "1" || $req_headers->{'x-bastion-allow-downgrade'} eq "0") {
|
|
$allow_downgrade = $req_headers->{'x-bastion-allow-downgrade'} + 0;
|
|
}
|
|
else {
|
|
return $self->log_and_exit(400, "Bad Request (invalid allow-downgrade value)", "Expected value '0', '1' or no header", {comment => "bad_allow_downgrade_value"});
|
|
}
|
|
}
|
|
|
|
# if there's an enforce-secure header, get it
|
|
my $enforce_secure = 0;
|
|
if ($req_headers->{'x-bastion-enforce-secure'}) {
|
|
if ($req_headers->{'x-bastion-enforce-secure'} eq "1" || $req_headers->{'x-bastion-enforce-secure'} eq "0") {
|
|
$enforce_secure = $req_headers->{'x-bastion-enforce-secure'} + 0;
|
|
}
|
|
else {
|
|
return $self->log_and_exit(400, "Bad Request (invalid enforce-secure value)", "Expected value '0', '1' or no header", {comment => "bad_enforce_secure_value"});
|
|
}
|
|
}
|
|
|
|
# here, we know the account is right, so we sudo to this account to proceed
|
|
my @cmd = ("sudo", "-n", "-u", $account, "--", "/usr/bin/env", "perl", "-T", "/opt/bastion/bin/proxy/osh-http-proxy-worker");
|
|
push @cmd, "--account", $account, "--context", $context, "--user", $user, "--host", $remotemachine, "--uniqid", $ENV{'UNIQID'};
|
|
push @cmd, "--method", $self->{'request_info'}{'request_method'}, "--path", $self->{'request_info'}{'request_path'};
|
|
push @cmd, "--port", $remoteport;
|
|
push @cmd, "--group", $group if $group;
|
|
push @cmd, "--timeout", $timeout if $timeout;
|
|
push @cmd, "--allow-downgrade" if $allow_downgrade;
|
|
push @cmd, "--insecure" if ($self->{'proxy_config'}{'insecure'} && !$enforce_secure);
|
|
foreach my $key (qw{ accept content-type connection }) {
|
|
push @cmd, "--header", $key . ':' . $req_headers->{$key} if (defined $req_headers->{$key});
|
|
}
|
|
|
|
# we don't want the CGI module to parse/modify/interpret the content, so we
|
|
# fake an application/xml content, this has the effect in the CGI module code
|
|
# to not mess at all with the data, which is what we want. This way we can get the
|
|
# raw unparsed/unmodified data through the special 'XForms:Model' param. Once done,
|
|
# we simply restore the real content-type
|
|
my $real_content_type = $ENV{'CONTENT_TYPE'};
|
|
$ENV{'CONTENT_TYPE'} = 'application/xml';
|
|
my $content = CGI->new->param('XForms:Model');
|
|
$ENV{'CONTENT_TYPE'} = $real_content_type;
|
|
$ENV{'PROXY_POST_DATA'} = encode_base64($content);
|
|
|
|
$ENV{'PROXY_ACCOUNT_PASSWORD'} = $pass;
|
|
undef $pass;
|
|
$self->{'_log'}{'request_body_length'} = length($content);
|
|
$fnret = OVH::Bastion::execute(cmd => \@cmd, noisy_stdout => 0, noisy_stderr => 1, is_helper => 1);
|
|
$fnret
|
|
or return $self->log_and_exit(500, "Internal Error (couldn't call helper)", "Couldn't call helper (" . $fnret->msg . ")", {comment => "exec1_failed"});
|
|
delete $ENV{'PROXY_ACCOUNT_PASSWORD'};
|
|
delete $ENV{'PROXY_POST_DATA'};
|
|
|
|
$fnret = OVH::Bastion::result_from_helper($fnret->value->{'stdout'});
|
|
$fnret
|
|
or return $self->log_and_exit(500, "Internal Error (helper execution failed)", "Helper execution failed (" . $fnret->msg . ")", {comment => "exec2_failed"});
|
|
|
|
$fnret = OVH::Bastion::helper_decapsulate($fnret->value);
|
|
$fnret
|
|
or return $self->log_and_exit(500, "Internal Error (helper returned an error)", "Helper returned an error (" . $fnret->msg . ")", {comment => "exec3_failed"});
|
|
|
|
if (ref $fnret->value->{'headers'} eq 'ARRAY') {
|
|
push @{$self->{'_supplementary_headers'}}, @{$fnret->value->{'headers'}};
|
|
}
|
|
if ($req_headers->{'accept-encoding'} =~ /gzip/) {
|
|
push @{$self->{'_supplementary_headers'}}, ['Content-Encoding', 'gzip'];
|
|
}
|
|
$self->{'request_info'}{'headers_sent'} = 1; # needed to avoid duplicate headers by our parent package
|
|
|
|
my $flattened_bastion2client_headers = _flatten_headers($self->{'_supplementary_headers'});
|
|
my $bastion2devicedelay = $flattened_bastion2client_headers->{'x-bastion-egress-timing'};
|
|
$self->{'_log'}{'post_content'} = $content;
|
|
$self->{'server'}{'access_log_format'} =
|
|
qq#%h $escaped_account $escaped_user %t "%r" %>s %b "$remotemachine" "$ENV{'UNIQID'}" "%{User-Agent}i" %D # . ($bastion2devicedelay || '-');
|
|
|
|
$self->{'_log'}{'bastion2device_delay'} = $bastion2devicedelay;
|
|
$self->{'_log'}{'allowed'} = $fnret->value->{'allowed'};
|
|
$self->log_and_exit($fnret->value->{'code'}, $fnret->value->{'msg'}, $fnret->value->{'body'}, {comment => "worker_returned"});
|
|
|
|
return 1;
|
|
}
|
|
|
|
# overrides parent func
|
|
sub http_base_headers {
|
|
my $self = shift;
|
|
my @headers = (
|
|
[Date => gmtime() . " GMT"],
|
|
[Connection => 'close'],
|
|
[Server => "The Bastion " . $OVH::Bastion::VERSION],
|
|
['X-Bastion-Instance' => hostname()],
|
|
['X-Bastion-ReqID' => $ENV{'UNIQID'}],
|
|
);
|
|
foreach my $keyval (@{$self->{'_supplementary_headers'}}) {
|
|
my $keyname = $keyval->[0];
|
|
$keyname = 'X-Bastion-Remote-' . $keyname if ($keyname =~ /^(client-ssl-)/i);
|
|
push @headers, [$keyname, $keyval->[1]];
|
|
}
|
|
delete $self->{'_supplementary_headers'};
|
|
return \@headers;
|
|
}
|
|
|
|
# this sub turns [ [ HeaDerA, ValueA ], [ HEAderB, ValueB ] ]
|
|
# into { headera => ValueA, headerb => ValueB }
|
|
# and errors if there is a duplicate header somewhere
|
|
sub _flatten_headers {
|
|
my $arrayref = shift;
|
|
my %headers;
|
|
|
|
if (ref $arrayref ne 'ARRAY') {
|
|
return "Bad call";
|
|
}
|
|
|
|
foreach my $keyval (@$arrayref) {
|
|
if (ref $keyval ne 'ARRAY' or @$keyval != 2) {
|
|
return "Malformed headers";
|
|
}
|
|
my $key = lc($keyval->[0]);
|
|
my $val = $keyval->[1];
|
|
if (exists($headers{$key})) {
|
|
return "Duplicate header $key";
|
|
}
|
|
$headers{$key} = $val;
|
|
}
|
|
return \%headers;
|
|
}
|
|
|
|
1;
|