use
5.008;
sub
_sanitise_input($);
our
$VERSION
=
'0.96'
;
our
$stdin_data
;
sub
new
{
my
$class
=
shift
;
my
%args
;
if
((
@_
== 1) && (
ref
$_
[0] eq
'HASH'
)) {
%args
= %{
$_
[0]};
}
elsif
((
scalar
(
@_
) % 2) == 0) {
%args
=
@_
;
}
else
{
carp(__PACKAGE__,
': Invalid arguments passed to new()'
);
return
;
}
if
(
defined
(
$args
{expect})) {
if
(
ref
(
$args
{expect}) ne
'ARRAY'
) {
Carp::carp(__PACKAGE__,
': expect must be a reference to an array'
);
return
;
}
Carp::croak(__PACKAGE__,
': expect is deprecated, use allow instead'
);
}
if
(!
defined
(
$class
)) {
if
((
scalar
keys
%args
) > 0) {
carp(__PACKAGE__,
' use ->new() not ::new() to instantiate'
);
return
;
}
$class
= __PACKAGE__;
}
elsif
(Scalar::Util::blessed(
$class
)) {
return
bless
{ %{
$class
},
%args
},
ref
(
$class
);
}
if
(
my
$logger
=
$args
{
'logger'
}) {
if
(!Scalar::Util::blessed(
$logger
)) {
$args
{
'logger'
} = Log::Abstraction->new(
$logger
);
}
}
else
{
$args
{
'logger'
} = Log::Abstraction->new();
}
return
bless
{
max_upload_size
=> 512 * 1024,
allow
=>
undef
,
upload_dir
=>
undef
,
%args
},
$class
;
}
sub
script_name
{
my
$self
=
shift
;
unless
(
$self
->{script_name}) {
$self
->_find_paths();
}
return
$self
->{script_name};
}
sub
_find_paths {
my
$self
=
shift
;
$self
->_trace(__PACKAGE__ .
': entering _find_paths'
);
require
File::Basename && File::Basename->
import
()
unless
File::Basename->can(
'basename'
);
my
$script_name
=
$self
->_get_env(
'SCRIPT_NAME'
) // $0;
$self
->{script_name} =
$self
->_untaint_filename({
filename
=> File::Basename::basename(
$script_name
)
});
if
(
my
$script_path
=
$self
->_get_env(
'SCRIPT_FILENAME'
)) {
$self
->{script_path} =
$script_path
;
}
elsif
(
$script_name
=
$self
->_get_env(
'SCRIPT_NAME'
)) {
if
(
my
$document_root
=
$self
->_get_env(
'DOCUMENT_ROOT'
)) {
$script_name
=
$self
->_get_env(
'SCRIPT_NAME'
);
$script_name
=~ s{^/}{};
$self
->{script_path} = File::Spec->catfile(
$document_root
,
$script_name
);
}
else
{
if
(File::Spec->file_name_is_absolute(
$script_name
) && (-r
$script_name
)) {
$self
->{script_path} =
$script_name
;
}
else
{
require
Cwd
unless
Cwd->can(
'abs_path'
);
if
(
$script_name
=~ /^\/(.+)/) {
$script_name
= $1;
}
$self
->{script_path} = File::Spec->catfile(Cwd::abs_path(),
$script_name
);
}
}
}
elsif
(File::Spec->file_name_is_absolute($0)) {
$self
->{script_path} = $0;
}
else
{
$self
->{script_path} = File::Spec->rel2abs($0);
}
$self
->{script_path} =
$self
->_untaint_filename({
filename
=>
$self
->{script_path}
});
}
sub
script_path {
my
$self
=
shift
;
unless
(
$self
->{script_path}) {
$self
->_find_paths();
}
return
$self
->{script_path};
}
sub
script_dir
{
my
$self
=
shift
;
$self
= __PACKAGE__->new()
unless
ref
$self
;
$self
->_find_paths()
unless
$self
->{script_path};
my
$dir_regex
= $^O eq
'MSWin32'
?
qr{(.+)\\.+?$}
:
qr{(.+)/.+?$}
;
return
$self
->{script_path} =~
$dir_regex
? $1 :
$self
->{script_path};
}
sub
host_name {
my
$self
=
shift
;
unless
(
$self
->{site}) {
$self
->_find_site_details();
}
return
$self
->{site};
}
sub
_find_site_details
{
my
$self
=
shift
;
$self
->_trace(
'Entering _find_site_details'
);
return
if
$self
->{site} &&
$self
->{cgi_site};
if
(
my
$host
=
$ENV
{
'HTTP_HOST'
} ||
$ENV
{
'SERVER_NAME'
}) {
$self
->{cgi_site} = URI::Heuristic::uf_uristr(
$host
);
$self
->{cgi_site} =~ s/(.*)\.+$/$1/;
if
(
$ENV
{
'SERVER_NAME'
} && (
$host
eq
$ENV
{
'SERVER_NAME'
}) && (
my
$protocol
=
$self
->protocol()) &&
$self
->protocol() ne
'http'
) {
$self
->{cgi_site} =~ s/^http/
$protocol
/;
}
}
else
{
$self
->_debug(
'Falling back to using hostname'
);
$self
->{cgi_site} = Sys::Hostname::hostname();
}
$self
->{site} ||=
$self
->{cgi_site};
$self
->{site} =~ s/^https?:\/\/(.+)/$1/;
$self
->{cgi_site} = (
$self
->protocol() ||
'http'
) .
'://'
.
$self
->{cgi_site}
unless
$self
->{cgi_site} =~ /^https?:\/\//;
$self
->_warn(
'Could not determine site name'
)
unless
(
$self
->{site} &&
$self
->{cgi_site});
$self
->_trace(
'Leaving _find_site_details'
);
}
sub
domain_name {
my
$self
=
shift
;
return
$self
->{domain}
if
$self
->{domain};
$self
->_find_site_details();
if
(
my
$site
=
$self
->{site}) {
$self
->{domain} = (
$site
=~ /^www\.(.+)/) ? $1 :
$site
;
}
return
$self
->{domain};
}
sub
cgi_host_url {
my
$self
=
shift
;
unless
(
$self
->{cgi_site}) {
$self
->_find_site_details();
}
return
$self
->{cgi_site};
}
sub
params {
my
$self
=
shift
;
my
$params
= Params::Get::get_params(
undef
,
@_
);
if
((
defined
(
$self
->{paramref})) && ((!
defined
(
$params
->{
'allow'
})) ||
defined
(
$self
->{allow}) && (
$params
->{
'allow'
} eq
$self
->{allow}))) {
return
$self
->{paramref};
}
if
(
defined
(
$params
->{allow})) {
$self
->{allow} =
$params
->{allow};
}
if
(
defined
(
$params
->{upload_dir})) {
$self
->{upload_dir} =
$params
->{upload_dir};
}
if
(
defined
(
$params
->{
'logger'
})) {
$self
->set_logger(
$params
->{
'logger'
});
}
$self
->_trace(
'Entering params'
);
my
@pairs
;
my
$content_type
=
$ENV
{
'CONTENT_TYPE'
};
my
%FORM
;
if
((!
$ENV
{
'GATEWAY_INTERFACE'
}) || (!
$ENV
{
'REQUEST_METHOD'
})) {
if
(
@ARGV
) {
@pairs
=
@ARGV
;
if
(
defined
(
$pairs
[0])) {
if
(
$pairs
[0] eq
'--robot'
) {
$self
->{is_robot} = 1;
shift
@pairs
;
}
elsif
(
$pairs
[0] eq
'--mobile'
) {
$self
->{is_mobile} = 1;
shift
@pairs
;
}
elsif
(
$pairs
[0] eq
'--search-engine'
) {
$self
->{is_search_engine} = 1;
shift
@pairs
;
}
elsif
(
$pairs
[0] eq
'--tablet'
) {
$self
->{is_tablet} = 1;
shift
@pairs
;
}
}
}
elsif
(
$stdin_data
) {
@pairs
=
split
(/\n/,
$stdin_data
);
}
elsif
(!
$self
->{args_read}) {
my
$oldfh
=
select
(STDOUT);
print
"Entering debug mode\n"
,
"Enter key=value pairs - end with quit\n"
;
select
(
$oldfh
);
$self
->{args_read} = 1;
while
(<STDIN>) {
chop
(
my
$line
=
$_
);
$line
=~ s/[\r\n]//g;
last
if
$line
eq
'quit'
;
push
(
@pairs
,
$line
);
$stdin_data
.=
"$line\n"
;
}
}
}
elsif
((
$ENV
{
'REQUEST_METHOD'
} eq
'GET'
) || (
$ENV
{
'REQUEST_METHOD'
} eq
'HEAD'
)) {
if
(
my
$query
=
$ENV
{
'QUERY_STRING'
}) {
if
((
defined
(
$content_type
)) && (
$content_type
=~ /multipart\/form-data/i)) {
$self
->_warn(
'Multipart/form-data not supported for GET'
);
}
$query
=~ s/\\u0026/\&/g;
@pairs
=
split
(/&/,
$query
);
}
else
{
return
;
}
}
elsif
(
$ENV
{
'REQUEST_METHOD'
} eq
'POST'
) {
if
(!
defined
(
$ENV
{
'CONTENT_LENGTH'
})) {
$self
->{status} = 411;
return
;
}
my
$content_length
=
$ENV
{
'CONTENT_LENGTH'
};
if
((
$self
->{max_upload_size} >= 0) && (
$content_length
>
$self
->{max_upload_size})) {
$self
->{status} = 413;
$self
->_warn(
'Large upload prohibited'
);
return
;
}
if
((!
defined
(
$content_type
)) || (
$content_type
=~ /application\/x-www-form-urlencoded/)) {
my
$buffer
;
if
(
$stdin_data
) {
$buffer
=
$stdin_data
;
}
else
{
if
(
read
(STDIN,
$buffer
,
$content_length
) !=
$content_length
) {
$self
->_warn(
'POST failed: something else may have read STDIN'
);
}
$stdin_data
=
$buffer
;
}
@pairs
=
split
(/&/,
$buffer
);
}
elsif
(
$content_type
=~ /multipart\/form-data/i) {
if
(!
defined
(
$self
->{upload_dir})) {
$self
->_warn({
warning
=>
'Attempt to upload a file when upload_dir has not been set'
});
return
;
}
if
(!File::Spec->file_name_is_absolute(
$self
->{upload_dir})) {
$self
->_warn({
warning
=>
"upload_dir $self->{upload_dir} isn't a full pathname"
});
$self
->status(500);
delete
$self
->{upload_dir};
return
;
}
if
(!-d
$self
->{upload_dir}) {
$self
->_warn({
warning
=>
"upload_dir $self->{upload_dir} isn't a directory"
});
$self
->status(500);
delete
$self
->{upload_dir};
return
;
}
if
(!-w
$self
->{upload_dir}) {
delete
$self
->{paramref};
$self
->_warn({
warning
=>
"upload_dir $self->{upload_dir} isn't writeable"
});
$self
->status(500);
delete
$self
->{upload_dir};
return
;
}
my
$tmpdir
=
$self
->tmpdir();
if
(
$self
->{
'upload_dir'
} !~ /^\Q
$tmpdir
\E/) {
$self
->_warn({
warning
=>
'upload_dir '
.
$self
->{
'upload_dir'
} .
" isn't somewhere in the temporary area $tmpdir"
});
$self
->status(500);
delete
$self
->{upload_dir};
return
;
}
if
(
$content_type
=~ /boundary=(\S+)$/) {
@pairs
=
$self
->_multipart_data({
length
=>
$content_length
,
boundary
=> $1
});
}
}
elsif
(
$content_type
=~ /text\/xml/i) {
my
$buffer
;
if
(
$stdin_data
) {
$buffer
=
$stdin_data
;
}
else
{
if
(
read
(STDIN,
$buffer
,
$content_length
) !=
$content_length
) {
$self
->_warn({
warning
=>
'XML failed: something else may have read STDIN'
});
}
$stdin_data
=
$buffer
;
}
$FORM
{XML} =
$buffer
;
$self
->{paramref} = \
%FORM
;
return
\
%FORM
;
}
elsif
(
$content_type
=~ /application\/json/i) {
my
$buffer
;
if
(
$stdin_data
) {
$buffer
=
$stdin_data
;
}
else
{
require
JSON::MaybeXS && JSON::MaybeXS->
import
()
unless
JSON::MaybeXS->can(
'parse_json'
);
if
(
read
(STDIN,
$buffer
,
$content_length
) !=
$content_length
) {
$self
->_warn({
warning
=>
'read failed: something else may have read STDIN'
});
}
$stdin_data
=
$buffer
;
my
$paramref
= decode_json(
$buffer
);
foreach
my
$key
(
keys
(%{
$paramref
})) {
push
@pairs
,
"$key="
.
$paramref
->{
$key
};
}
}
}
else
{
my
$buffer
;
if
(
$stdin_data
) {
$buffer
=
$stdin_data
;
}
else
{
if
(
read
(STDIN,
$buffer
,
$content_length
) !=
$content_length
) {
$self
->_warn({
warning
=>
'read failed: something else may have read STDIN'
});
}
$stdin_data
=
$buffer
;
}
$self
->_warn({
warning
=>
"POST: Invalid or unsupported content type: $content_type: $buffer"
,
});
}
}
elsif
(
$ENV
{
'REQUEST_METHOD'
} eq
'OPTIONS'
) {
$self
->{status} = 405;
return
;
}
elsif
(
$ENV
{
'REQUEST_METHOD'
} eq
'DELETE'
) {
$self
->{status} = 405;
return
;
}
else
{
$self
->{status} = 501;
$self
->_warn({
warning
=>
'Use POST, GET or HEAD'
});
}
unless
(
scalar
@pairs
) {
return
;
}
String::Clean::XSS->
import
();
foreach
my
$arg
(
@pairs
) {
my
(
$key
,
$value
) =
split
(/=/,
$arg
, 2);
next
unless
(
$key
);
$key
=~ s/%00//g;
$key
=~ s/%([a-fA-F\d][a-fA-F\d])/
pack
(
"C"
,
hex
($1))/eg;
$key
=~
tr
/+/ /;
if
(
defined
(
$value
)) {
$value
=~ s/%00//g;
$value
=~ s/%([a-fA-F\d][a-fA-F\d])/
pack
(
"C"
,
hex
($1))/eg;
$value
=~
tr
/+/ /;
}
else
{
$value
=
''
;
}
$key
= _sanitise_input(
$key
);
if
(
$self
->{allow}) {
if
(!
exists
(
$self
->{allow}->{
$key
})) {
$self
->_notice(
"Discard unallowed argument '$key'"
);
$self
->status(422);
next
;
}
if
(
defined
(
my
$schema
=
$self
->{allow}->{
$key
})) {
if
(!
ref
(
$schema
)) {
if
(
$value
ne
$schema
) {
$self
->_notice(
"Block $key = $value"
);
$self
->status(422);
next
;
}
}
elsif
(
ref
(
$schema
) eq
'Regexp'
) {
if
(
$value
!~
$schema
) {
$self
->_notice(
"Block $key = $value"
);
$self
->status(422);
next
;
}
}
else
{
eval
{
$value
= Params::Validate::Strict::validate_strict({
schema
=> {
$key
=>
$schema
},
args
=> {
$key
=>
$value
},
unknown_parameter_handler
=>
'warn'
,
});
};
if
($@) {
$self
->_notice(
"Block $key = $value: $@"
);
$self
->status(422);
next
;
}
$value
=
$value
->{
$key
};
}
}
}
my
$orig_value
=
$value
;
$value
= _sanitise_input(
$value
);
if
((!
defined
(
$ENV
{
'REQUEST_METHOD'
})) || (
$ENV
{
'REQUEST_METHOD'
} eq
'GET'
)) {
if
((
$value
=~ /(\%27)|(\')|(\%23)|(\
(
$value
=~ /((\%3D)|(=))[^\n]*((\%27)|(\')|(\-\-)|(\%3B)|(;))/i) ||
(
$value
=~ /\w*((\%27)|(\'))((\%6F)|o|(\%4F))((\%72)|r|(\%52))/ix) ||
(
$value
=~ /((\%27)|(\'))union/ix) ||
(
$value
=~ /
select
[[a-z]\s\*]from/ix) ||
(
$value
=~ /\sAND\s1=1/ix) ||
(
$value
=~ /\sOR\s.+\sAND\s/) ||
(
$value
=~ /\/\*\*\/ORDER\/\*\*\/BY\/\*\*/ix) ||
(
$value
=~ /
exec
(\s|\+)+(s|x)p\w+/ix)) {
$self
->status(403);
if
(
$ENV
{
'REMOTE_ADDR'
}) {
$self
->_warn(
$ENV
{
'REMOTE_ADDR'
} .
": SQL injection attempt blocked for '$value'"
);
}
else
{
$self
->_warn(
"SQL injection attempt blocked for '$value'"
);
}
return
;
}
if
(
my
$agent
=
$ENV
{
'HTTP_USER_AGENT'
}) {
if
((
$agent
=~ /SELECT.+AND.+/) || (
$agent
=~ /ORDER BY /) || (
$agent
=~ / OR NOT /) || (
$agent
=~ / AND \d+=\d+/) || (
$agent
=~ /THEN.+ELSE.+END/) || (
$agent
=~ /.+AND.+SELECT.+/) || (
$agent
=~ /\sAND\s.+\sAND\s/)) {
$self
->status(403);
if
(
$ENV
{
'REMOTE_ADDR'
}) {
$self
->_warn(
$ENV
{
'REMOTE_ADDR'
} .
": SQL injection attempt blocked for '$agent'"
);
}
else
{
$self
->_warn(
"SQL injection attempt blocked for '$agent'"
);
}
return
1;
}
}
if
((
$value
=~ /((\%3C)|<)((\%2F)|\/)*[a-z0-9\%]+((\%3E)|>)/ix) ||
(
$value
=~ /((\%3C)|<)[^\n]+((\%3E)|>)/i) ||
(
$orig_value
=~ /((\%3C)|<)((\%2F)|\/)*[a-z0-9\%]+((\%3E)|>)/ix) ||
(
$orig_value
=~ /((\%3C)|<)[^\n]+((\%3E)|>)/i)) {
$self
->status(403);
$self
->_warn(
"XSS injection attempt blocked for '$value'"
);
return
;
}
if
(
$value
=~ /\.\.\//) {
$self
->status(403);
$self
->_warn(
"Blocked directory traversal attack for '$key'"
);
return
;
}
if
(
$value
=~ /mustleak\.com\//) {
$self
->status(403);
$self
->_warn(
"Blocked mustleak attack for '$key'"
);
return
;
}
}
if
(
length
(
$value
) > 0) {
if
(
$FORM
{
$key
} && (
$FORM
{
$key
} ne
$value
)) {
$FORM
{
$key
} .=
",$value"
;
}
else
{
$FORM
{
$key
} =
$value
;
}
}
}
unless
(
%FORM
) {
return
;
}
if
(
$self
->{
'logger'
}) {
while
(
my
(
$key
,
$value
) =
each
%FORM
) {
$self
->_debug(
"$key=$value"
);
}
}
$self
->{paramref} = \
%FORM
;
return
\
%FORM
;
}
sub
param {
my
(
$self
,
$field
) =
@_
;
if
(!
defined
(
$field
)) {
return
$self
->params();
}
if
(
$self
->{allow} && !
exists
(
$self
->{allow}->{
$field
})) {
$self
->_warn({
warning
=>
"param: $field isn't in the allow list"
});
return
;
}
if
(
defined
(
$self
->params())) {
return
$self
->params()->{
$field
};
}
return
;
}
sub
_sanitise_input($) {
my
$arg
=
shift
;
$arg
=~ s/[\r\n]//g;
$arg
=~ s/\s+$//;
$arg
=~ s/^\s//;
$arg
=~ s/<!--.*-->//g;
return
convert_XSS(
$arg
);
}
sub
_multipart_data {
my
(
$self
,
$args
) =
@_
;
$self
->_trace(
'Entering _multipart_data'
);
my
$total_bytes
=
$$args
{
length
};
$self
->_debug(
"_multipart_data: total_bytes = $total_bytes"
);
if
(
$total_bytes
== 0) {
return
;
}
unless
(
$stdin_data
) {
while
(<STDIN>) {
chop
(
my
$line
=
$_
);
$line
=~ s/[\r\n]//g;
$stdin_data
.=
"$line\n"
;
}
if
(!
$stdin_data
) {
return
;
}
}
my
$boundary
=
$$args
{boundary};
my
@pairs
;
my
$writing_file
= 0;
my
$key
;
my
$value
;
my
$in_header
= 0;
my
$fout
;
foreach
my
$line
(
split
(/\n/,
$stdin_data
)) {
if
(
$line
=~ /^--\Q
$boundary
\E--$/) {
last
;
}
if
(
$line
=~ /^--\Q
$boundary
\E$/) {
if
(
$writing_file
) {
close
$fout
;
$writing_file
= 0;
}
elsif
(
defined
(
$key
)) {
push
(
@pairs
,
"$key=$value"
);
$value
=
undef
;
}
$in_header
= 1;
}
elsif
(
$in_header
) {
if
(
length
(
$line
) == 0) {
$in_header
= 0;
}
elsif
(
$line
=~ /^Content-Disposition: (.+)/i) {
my
$field
= $1;
if
(
$field
=~ /name=
"(.+?)"
/) {
$key
= $1;
}
if
(
$field
=~ /filename=
"(.+)?"
/) {
my
$filename
= $1;
unless
(
defined
(
$filename
)) {
$self
->_warn(
'No upload filename given'
);
}
elsif
(
$filename
=~ /[\\\/\|]/) {
$self
->_warn(
"Disallowing invalid filename: $filename"
);
}
else
{
$filename
=
$self
->_create_file_name({
filename
=>
$filename
});
my
$full_path
= File::Spec->catfile(
$self
->{upload_dir},
$filename
);
unless
(
open
(
$fout
,
'>'
,
$full_path
)) {
$self
->_warn(
"Can't open $full_path"
);
}
$writing_file
= 1;
push
(
@pairs
,
"$key=$filename"
);
}
}
}
}
else
{
if
(
$writing_file
) {
print
$fout
"$line\n"
;
}
else
{
$value
.=
$line
;
}
}
}
if
(
$writing_file
) {
close
$fout
;
}
$self
->_trace(
'Leaving _multipart_data'
);
return
@pairs
;
}
sub
_create_file_name {
my
(
$self
,
$args
) =
@_
;
my
$filename
=
$$args
{filename} .
'_'
.
time
;
my
$counter
= 0;
my
$rc
;
do
{
$rc
=
$filename
. (
$counter
?
"_$counter"
:
''
);
$counter
++;
}
until
(! -e
$rc
);
return
$rc
;
}
sub
_untaint_filename {
my
(
$self
,
$args
) =
@_
;
if
(
$$args
{filename} =~ /(^[\w\+_\040\
return
$1;
}
}
sub
is_mobile {
my
$self
=
shift
;
if
(
defined
(
$self
->{is_mobile})) {
return
$self
->{is_mobile};
}
if
(
my
$ch_ua_mobile
=
$ENV
{
'HTTP_SEC_CH_UA_MOBILE'
}) {
if
(
$ch_ua_mobile
eq
'?1'
) {
$self
->{is_mobile} = 1;
return
1;
}
}
if
(
$ENV
{
'HTTP_X_WAP_PROFILE'
}) {
$self
->{is_mobile} = 1;
return
1;
}
if
(
my
$agent
=
$ENV
{
'HTTP_USER_AGENT'
}) {
if
(
$agent
=~ /.+(Android|iPhone).+/) {
$self
->{is_mobile} = 1;
return
1;
}
if
(
$agent
=~ m/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|
link
)|vodafone|wap|windows ce|xda|xiino/i ||
substr
(
$ENV
{
'HTTP_USER_AGENT'
}, 0, 4) =~ m/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|
do
(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(
no
|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|
do
|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|
uc
)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i) {
$self
->{is_mobile} = 1;
return
1;
}
my
$remote
=
$ENV
{
'REMOTE_ADDR'
};
if
(
defined
(
$remote
) &&
$self
->{cache}) {
if
(
my
$type
=
$self
->{cache}->get(
"$remote/$agent"
)) {
return
$self
->{is_mobile} = (
$type
eq
'mobile'
);
}
}
unless
(
$self
->{browser_detect}) {
HTTP::BrowserDetect->
import
();
$self
->{browser_detect} = HTTP::BrowserDetect->new(
$agent
);
}
}
if
(
$self
->{browser_detect}) {
my
$device
=
$self
->{browser_detect}->device();
my
$is_mobile
= (
defined
(
$device
) && (
$device
=~ /blackberry|webos|iphone|ipod|ipad|android/i)) ? 1 : 0;
if
(
$is_mobile
&&
$self
->{cache} &&
defined
(
$remote
)) {
$self
->{cache}->set(
"$remote/$agent"
,
'mobile'
,
'1 day'
);
}
return
$self
->{is_mobile} =
$is_mobile
;
}
}
return
0;
}
sub
is_tablet {
my
$self
=
shift
;
if
(
defined
(
$self
->{is_tablet})) {
return
$self
->{is_tablet};
}
if
(
$ENV
{
'HTTP_USER_AGENT'
} && (
$ENV
{
'HTTP_USER_AGENT'
} =~ /.+(iPad|TabletPC).+/)) {
$self
->{is_tablet} = 1;
}
else
{
$self
->{is_tablet} = 0;
}
return
$self
->{is_tablet};
}
sub
as_string
{
my
$self
=
shift
;
my
$params
=
$self
->params() ||
return
''
;
my
$args
= Params::Get::get_params(
undef
,
@_
);
my
$rc
;
if
(
$args
->{
'raw'
}) {
$rc
=
join
'; '
,
map
{
"$_="
.
$params
->{
$_
}
}
sort
keys
%{
$params
};
}
else
{
$rc
=
join
'; '
,
map
{
my
$value
=
$params
->{
$_
};
$value
=~ s/\\/\\\\/g;
$value
=~ s/(;|=)/\\$1/g;
"$_=$value"
}
sort
keys
%{
$params
};
}
$self
->_trace(
"as_string: returning '$rc'"
)
if
(
$rc
);
return
$rc
;
}
sub
protocol {
my
$self
=
shift
;
if
(
$ENV
{
'SCRIPT_URI'
} && (
$ENV
{
'SCRIPT_URI'
} =~ /^(.+):\/\/.+/)) {
return
$1;
}
if
(
$ENV
{
'SERVER_PROTOCOL'
} && (
$ENV
{
'SERVER_PROTOCOL'
} =~ /^HTTP\//)) {
return
'http'
;
}
if
(
my
$port
=
$ENV
{
'SERVER_PORT'
}) {
if
(
defined
(
my
$name
=
getservbyport
(
$port
,
'tcp'
))) {
if
(
$name
=~ /https?/) {
return
$name
;
}
elsif
(
$name
eq
'www'
) {
return
'http'
;
}
}
elsif
(
$port
== 80) {
return
'http'
;
}
elsif
(
$port
== 443) {
return
'https'
;
}
}
if
(
$ENV
{
'REMOTE_ADDR'
}) {
$self
->_warn(
"Can't determine the calling protocol"
);
}
return
;
}
sub
tmpdir {
my
$self
=
shift
;
my
$name
=
'tmp'
;
if
($^O eq
'MSWin32'
) {
$name
=
'temp'
;
}
my
$dir
;
if
(!
ref
(
$self
)) {
$self
= __PACKAGE__->new();
}
my
$params
= Params::Get::get_params(
undef
,
@_
);
if
(
$ENV
{
'C_DOCUMENT_ROOT'
} && (-d
$ENV
{
'C_DOCUMENT_ROOT'
})) {
$dir
= File::Spec->catdir(
$ENV
{
'C_DOCUMENT_ROOT'
},
$name
);
if
((-d
$dir
) && (-w
$dir
)) {
return
$self
->_untaint_filename({
filename
=>
$dir
});
}
$dir
=
$ENV
{
'C_DOCUMENT_ROOT'
};
if
((-d
$dir
) && (-w
$dir
)) {
return
$self
->_untaint_filename({
filename
=>
$dir
});
}
}
if
(
$ENV
{
'DOCUMENT_ROOT'
} && (-d
$ENV
{
'DOCUMENT_ROOT'
})) {
$dir
= File::Spec->catdir(
$ENV
{
'DOCUMENT_ROOT'
}, File::Spec->updir(),
$name
);
if
((-d
$dir
) && (-w
$dir
)) {
return
$self
->_untaint_filename({
filename
=>
$dir
});
}
}
return
$params
->{
default
} ?
$params
->{
default
} : File::Spec->tmpdir();
}
sub
rootdir {
if
(
$ENV
{
'C_DOCUMENT_ROOT'
} && (-d
$ENV
{
'C_DOCUMENT_ROOT'
})) {
return
$ENV
{
'C_DOCUMENT_ROOT'
};
}
elsif
(
$ENV
{
'DOCUMENT_ROOT'
} && (-d
$ENV
{
'DOCUMENT_ROOT'
})) {
return
$ENV
{
'DOCUMENT_ROOT'
};
}
my
$script_name
= $0;
unless
(File::Spec->file_name_is_absolute(
$script_name
)) {
$script_name
= File::Spec->rel2abs(
$script_name
);
}
if
(
$script_name
=~ /.cgi\-bin.*/) {
$script_name
=~ s/.cgi\-bin.*//;
}
if
(-f
$script_name
) {
if
($^O eq
'MSWin32'
) {
if
(
$script_name
=~ /(.+)\\.+?$/) {
return
$1;
}
}
else
{
if
(
$script_name
=~ /(.+)\/.+?$/) {
return
$1;
}
}
}
return
$script_name
;
}
sub
root_dir
{
if
(
$_
[0] &&
ref
(
$_
[0])) {
my
$self
=
shift
;
return
$self
->rootdir(
@_
);
}
return
__PACKAGE__->rootdir(
@_
);
}
sub
documentroot
{
if
(
$_
[0] &&
ref
(
$_
[0])) {
my
$self
=
shift
;
return
$self
->rootdir(
@_
);
}
return
__PACKAGE__->rootdir(
@_
);
}
sub
logdir {
my
$self
=
shift
;
my
$dir
=
shift
;
if
(!
ref
(
$self
)) {
$self
= __PACKAGE__->new();
}
if
(
defined
(
$dir
)) {
return
$self
->{logdir} =
$dir
;
}
foreach
my
$rc
(
$self
->{logdir},
$ENV
{
'LOGDIR'
}, Sys::Path->logdir(),
$self
->tmpdir()) {
if
(
defined
(
$rc
) &&
length
(
$rc
) && (-d
$rc
) && (-w
$rc
)) {
$dir
=
$rc
;
last
;
}
}
carp(
"Can't determine logdir"
)
if
((!
defined
(
$dir
)) || (
length
(
$dir
) == 0));
$self
->{logdir} ||=
$dir
;
return
$dir
;
}
sub
is_robot {
my
$self
=
shift
;
if
(
defined
(
$self
->{is_robot})) {
return
$self
->{is_robot};
}
my
$agent
=
$ENV
{
'HTTP_USER_AGENT'
};
my
$remote
=
$ENV
{
'REMOTE_ADDR'
};
unless
(
$remote
&&
$agent
) {
return
0;
}
if
((
$agent
=~ /SELECT.+AND.+/) || (
$agent
=~ /ORDER BY /) || (
$agent
=~ / OR NOT /) || (
$agent
=~ / AND \d+=\d+/) || (
$agent
=~ /THEN.+ELSE.+END/) || (
$agent
=~ /.+AND.+SELECT.+/) || (
$agent
=~ /\sAND\s.+\sAND\s/)) {
$self
->status(403);
$self
->{is_robot} = 1;
if
(
$ENV
{
'REMOTE_ADDR'
}) {
$self
->_warn(
$ENV
{
'REMOTE_ADDR'
} .
": SQL injection attempt blocked for '$agent'"
);
}
else
{
$self
->_warn(
"SQL injection attempt blocked for '$agent'"
);
}
return
1;
}
if
(
$agent
=~ /.+bot|axios\/1\.6\.7|bytespider|ClaudeBot|msnptc|CriteoBot|is_archiver|backstreet|linkfluence\.com|spider|scoutjet|gingersoftware|heritrix|dodnetdotcom|yandex|nutch|ezooms|plukkie|nova\.6scan\.com|Twitterbot|adscanner|Go-http-client|python-requests|Mediatoolkitbot|NetcraftSurveyAgent|Expanse|serpstatbot|DreamHost SiteMonitor|techiaith.cymru|trendictionbot|ias_crawler|Yak\/1\.0|ZoominfoBot/i) {
$self
->{is_robot} = 1;
return
1;
}
my
$key
=
"$remote/$agent"
;
if
(
my
$referrer
=
$ENV
{
'HTTP_REFERER'
}) {
my
@crawler_lists
= (
);
$referrer
=~ s/\\/_/g;
if
((
$referrer
=~ /\)/) || (List::Util::any {
$_
=~ /^
$referrer
/ }
@crawler_lists
)) {
$self
->_debug(
"is_robot: blocked trawler $referrer"
);
if
(
$self
->{cache}) {
$self
->{cache}->set(
$key
,
'robot'
,
'1 day'
);
}
$self
->{is_robot} = 1;
return
1;
}
}
if
(
defined
(
$remote
) &&
$self
->{cache}) {
if
(
my
$type
=
$self
->{cache}->get(
"$remote/$agent"
)) {
return
$self
->{is_robot} = (
$type
eq
'robot'
);
}
}
if
(
$agent
=~ /www\.majestic12\.co\.uk|facebookexternal/) {
if
(
$self
->{cache}) {
$self
->{cache}->set(
$key
,
'search'
,
'1 day'
);
}
return
0;
}
unless
(
$self
->{browser_detect}) {
HTTP::BrowserDetect->
import
();
$self
->{browser_detect} = HTTP::BrowserDetect->new(
$agent
);
}
}
if
(
$self
->{browser_detect}) {
my
$is_robot
=
$self
->{browser_detect}->robot();
if
(
defined
(
$is_robot
)) {
$self
->_debug(
"HTTP::BrowserDetect '$ENV{HTTP_USER_AGENT}' returns $is_robot"
);
}
$is_robot
= (
defined
(
$is_robot
) && (
$is_robot
)) ? 1 : 0;
$self
->_debug(
"is_robot: $is_robot"
);
if
(
$is_robot
) {
if
(
$self
->{cache}) {
$self
->{cache}->set(
$key
,
'robot'
,
'1 day'
);
}
$self
->{is_robot} =
$is_robot
;
return
$is_robot
;
}
}
if
(
$self
->{cache}) {
$self
->{cache}->set(
$key
,
'unknown'
,
'1 day'
);
}
$self
->{is_robot} = 0;
return
0;
}
sub
is_search_engine {
my
$self
=
shift
;
if
(
defined
(
$self
->{is_search_engine})) {
return
$self
->{is_search_engine};
}
my
$remote
=
$ENV
{
'REMOTE_ADDR'
};
my
$agent
=
$ENV
{
'HTTP_USER_AGENT'
};
unless
(
$remote
&&
$agent
) {
return
0;
}
my
$key
;
if
(
$self
->{cache}) {
$key
=
"$remote/$agent"
;
if
(
defined
(
$remote
) &&
$self
->{cache}) {
if
(
my
$type
=
$self
->{cache}->get(
"$remote/$agent"
)) {
return
$self
->{is_search} = (
$type
eq
'search'
);
}
}
}
if
(
$agent
=~ /www\.majestic12\.co\.uk|facebookexternal/) {
if
(
$self
->{cache}) {
$self
->{cache}->set(
$key
,
'search'
,
'1 day'
);
}
return
1;
}
unless
(
$self
->{browser_detect}) {
HTTP::BrowserDetect->
import
();
$self
->{browser_detect} = HTTP::BrowserDetect->new(
$agent
);
}
}
if
(
my
$browser
=
$self
->{browser_detect}) {
my
$is_search
= (
$browser
->google() ||
$browser
->msn() ||
$browser
->baidu() ||
$browser
->altavista() ||
$browser
->yahoo() ||
$browser
->bingbot());
if
(!
$is_search
) {
if
((
$agent
=~ /SeznamBot\//) ||
(
$agent
=~ /Googlebot\//)) {
$is_search
= 1;
}
}
if
(
$is_search
&&
$self
->{cache}) {
$self
->{cache}->set(
$key
,
'search'
,
'1 day'
);
}
return
$self
->{is_search_engine} =
$is_search
;
}
my
$hostname
=
gethostbyaddr
(inet_aton(
$remote
), AF_INET) ||
$remote
;
if
(
defined
(
$hostname
) && (
$hostname
=~ /google|msnbot|bingbot|amazonbot|GPTBot/) && (
$hostname
!~ /^google-proxy/)) {
if
(
$self
->{cache}) {
$self
->{cache}->set(
$key
,
'search'
,
'1 day'
);
}
$self
->{is_search_engine} = 1;
return
1;
}
$self
->{is_search_engine} = 0;
return
0;
}
sub
browser_type {
my
$self
=
shift
;
if
(
$self
->is_mobile()) {
return
'mobile'
;
}
if
(
$self
->is_search_engine()) {
return
'search'
;
}
if
(
$self
->is_robot()) {
return
'robot'
;
}
return
'web'
;
}
sub
get_cookie {
my
$self
=
shift
;
my
$params
= Params::Get::get_params(
'cookie_name'
,
@_
);
if
(!
defined
(
$params
->{
'cookie_name'
})) {
$self
->_warn(
'cookie_name argument not given'
);
return
;
}
unless
(
$self
->{jar}) {
if
(
defined
$ENV
{
'HTTP_COOKIE'
}) {
$self
->{jar} = {
map
{
split
(/=/,
$_
, 2) }
split
(/; /,
$ENV
{
'HTTP_COOKIE'
}) };
}
}
return
$self
->{jar}->{
$params
->{
'cookie_name'
}};
}
sub
cookie {
my
(
$self
,
$field
) =
@_
;
if
(!
defined
(
$field
)) {
$self
->_warn(
'what cookie do you want?'
);
return
;
}
unless
(
$self
->{jar}) {
if
(
defined
$ENV
{
'HTTP_COOKIE'
}) {
$self
->{jar} = {
map
{
split
(/=/,
$_
, 2) }
split
(/; /,
$ENV
{
'HTTP_COOKIE'
}) };
}
}
return
$self
->{jar}{
$field
};
}
sub
status
{
my
$self
=
shift
;
my
$status
=
shift
;
return
$self
->{status} =
$status
if
(
defined
(
$status
));
unless
(
defined
$self
->{status}) {
my
$method
=
$ENV
{
'REQUEST_METHOD'
};
return
405
if
$method
&& (
$method
eq
'OPTIONS'
||
$method
eq
'DELETE'
);
return
411
if
$method
&& (
$method
eq
'POST'
&& !
defined
$ENV
{
'CONTENT_LENGTH'
});
return
200;
}
return
$self
->{status} || 200;
}
sub
messages
{
my
$self
=
shift
;
return
$self
->{
'messages'
};
}
sub
messages_as_string
{
my
$self
=
shift
;
if
(
scalar
(
$self
->{
'messages'
})) {
my
@messages
=
map
{
$_
->{
'message'
} } @{
$self
->{
'messages'
}};
return
join
(
'; '
,
@messages
);
}
return
''
;
}
sub
set_logger
{
my
$self
=
shift
;
my
$params
= Params::Get::get_params(
'logger'
,
@_
);
if
(
defined
(
$params
->{
'logger'
})) {
if
(
my
$logger
=
$params
->{
'logger'
}) {
if
(Scalar::Util::blessed(
$logger
)) {
$self
->{
'logger'
} =
$logger
;
}
else
{
$self
->{
'logger'
} = Log::Abstraction->new(
$logger
);
}
}
else
{
$self
->{
'logger'
} = Log::Abstraction->new();
}
return
$self
;
}
Carp::croak(
'Usage: set_logger(logger => $logger)'
)
}
sub
_log
{
my
(
$self
,
$level
,
@messages
) =
@_
;
push
@{
$self
->{
'messages'
}}, {
level
=>
$level
,
message
=>
join
(
' '
,
grep
defined
,
@messages
) };
if
(
my
$logger
=
$self
->{
'logger'
}) {
$self
->{
'logger'
}->
$level
(\
@messages
);
}
}
sub
_debug {
my
$self
=
shift
;
$self
->_log(
'debug'
,
@_
);
}
sub
_info {
my
$self
=
shift
;
$self
->_log(
'info'
,
@_
);
}
sub
_notice {
my
$self
=
shift
;
$self
->_log(
'notice'
,
@_
);
}
sub
_trace {
my
$self
=
shift
;
$self
->_log(
'trace'
,
@_
);
}
sub
_warn {
my
$self
=
shift
;
my
$params
= Params::Get::get_params(
'warning'
,
@_
);
$self
->_log(
'warn'
,
$params
->{
'warning'
});
}
sub
_get_env
{
my
(
$self
,
$var
) =
@_
;
return
unless
defined
$ENV
{
$var
};
if
(
$ENV
{
$var
} =~ /^[\w\.\-\/:\\]+$/) {
return
$ENV
{
$var
};
}
$self
->_warn(
"Invalid value in environment variable: $var"
);
return
undef
;
}
sub
reset
{
my
$class
=
shift
;
unless
(
$class
eq __PACKAGE__) {
carp(
'Reset is a class method'
);
return
;
}
$stdin_data
=
undef
;
}
sub
AUTOLOAD
{
our
$AUTOLOAD
;
my
$self
=
shift
or
return
;
Carp::croak(__PACKAGE__,
": Unknown method $self"
)
if
(!
ref
(
$self
));
Carp::croak(__PACKAGE__,
": Unknown method $self"
)
if
(
exists
(
$self
->{
'auto_load'
}) &&
$self
->{
'auto_load'
}->isFalse());
my
(
$method
) =
$AUTOLOAD
=~ /::(\w+)$/;
return
if
$method
eq
'DESTROY'
;
return
unless
ref
(
$self
) eq __PACKAGE__;
return
$self
->param(
$method
);
}
1;