# apache2 -- lintian check script -*- perl -*- # # Copyright (C) 2012 Arno Toell # Copyright (C) 2017-2018 Chris Lamb # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, you can find it on the World Wide # Web at https://www.gnu.org/copyleft/gpl.html, or write to the Free # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, # MA 02110-1301, USA. package Lintian::Check::Apache2; use v5.20; use warnings; use utf8; use File::Basename; use Unicode::UTF8 qw(encode_utf8); use Lintian::Relation; use Moo; use namespace::clean; with 'Lintian::Check'; # whether the package appears to be an Apache2 module/web application has is_apache2_related => ( is => 'rw', lazy => 1, default => sub { my ($self) = @_; for my $item (@{$self->processable->installed->sorted_list}) { return 1 if $item->name =~ m{^ usr/lib/apache2/modules/ }x && $item->basename =~ m{ [.]so $}x; return 1 if $item->name =~ m{^ etc/apache2/ (?:conf|site) - (?:available|enabled) / }x; return 1 if $item->name =~ m{^ etc/apache2/conf[.]d/}x; } return 0; } ); sub visit_installed_files { my ($self, $item) = @_; # Do nothing if the package in question appears to be related to # the web server itself return if $self->processable->name =~ m/^apache2(:?\.2)?(?:-\w+)?$/; # File is probably not relevant to us, ignore it return if $item->is_dir; return if $item->name !~ m{^(?:usr/lib/apache2/modules/|etc/apache2/)}; # Package installs an unrecognized file - check this for all files if ( $item->name !~ /\.conf$/ && $item->name =~ m{^etc/apache2/(conf|site|mods)-available/(.*)$}){ my $temp_type = $1; my $temp_file = $2; # ... except modules which are allowed to ship .load files $self->pointed_hint('apache2-configuration-files-need-conf-suffix', $item->pointer) unless $temp_type eq 'mods' && $temp_file =~ /\.load$/; } # Package appears to be a binary module if ($item->name =~ m{^usr/lib/apache2/modules/(.*)\.so$}) { $self->check_module_package($item, $1); } # Package appears to be a web application elsif ($item->name =~ m{^etc/apache2/(conf|site)-available/(.*)$}) { $self->check_web_application_package($item, $1, $2); } # Package appears to be a legacy web application elsif ($item->name =~ m{^etc/apache2/conf\.d/(.*)$}) { $self->pointed_hint( 'apache2-reverse-dependency-uses-obsolete-directory', $item->pointer); $self->check_web_application_package($item,'conf', $1); } # Package does scary things elsif ($item->name =~ m{^etc/apache2/(?:conf|sites|mods)-enabled/.*$}) { $self->pointed_hint( 'apache2-reverse-dependency-ships-file-in-not-allowed-directory', $item->pointer); } return; } sub installable { my ($self) = @_; # Do nothing if the package in question appears to be related to # the web server itself return if $self->processable->name =~ m/^apache2(:?\.2)?(?:-\w+)?$/; return; } sub check_web_application_package { my ($self, $item, $pkgtype, $webapp) = @_; my $pkg = $self->processable->name; my $processable = $self->processable; $self->pointed_hint('non-standard-apache2-configuration-name', $item->pointer, "$webapp != $pkg.conf") if $webapp ne "$pkg.conf" || $webapp =~ /^local-/; my $rel = $processable->relation('strong') ->logical_and($processable->relation('Recommends')); # A web application must not depend on apache2-whatever my $visit = sub { if (m/^apache2(?:\.2)?-(?:common|data|bin)$/) { $self->pointed_hint( 'web-application-depends-on-apache2-data-package', $item->pointer, $_, $webapp); return 1; } return 0; }; $rel->visit($visit, Lintian::Relation::VISIT_STOP_FIRST_MATCH); # ... nor on apache2 only. Moreover, it should be in the form # apache2 | httpd but don't worry about versions, virtual package # don't support that $self->pointed_hint('web-application-works-only-with-apache', $item->pointer, $webapp) if $rel->satisfies('apache2'); $self->inspect_conf_file($pkgtype, $item); return; } sub check_module_package { my ($self, $item, $module) = @_; my $pkg = $self->processable->name; my $processable = $self->processable; # We want packages to be follow our naming scheme. Modules should be named # libapache2-mod- if it ships a mod_foo.so # NB: Some modules have uppercase letters in them (e.g. Ruwsgi), but # obviously the package should be in all lowercase. my $expected_name = 'libapache2-' . lc($module); my $rel; $expected_name =~ tr/_/-/; $self->pointed_hint('non-standard-apache2-module-package-name', $item->pointer, "$pkg != $expected_name") if $expected_name ne $pkg; $rel = $processable->relation('strong') ->logical_and($processable->relation('Recommends')); $self->pointed_hint('apache2-module-does-not-depend-on-apache2-api', $item->pointer) if !$rel->matches(qr/^apache2-api-\d+$/); # The module is called mod_foo.so, thus the load file is expected to be # named foo.load my $load_file = $module; my $conf_file = $module; $load_file =~ s{^mod.(.*)$}{etc/apache2/mods-available/$1.load}; $conf_file =~ s{^mod.(.*)$}{etc/apache2/mods-available/$1.conf}; if (my $f = $processable->installed->lookup($load_file)) { $self->inspect_conf_file('mods', $f); } else { $self->pointed_hint('apache2-module-does-not-ship-load-file', $item->pointer, $load_file); } if (my $f = $processable->installed->lookup($conf_file)) { $self->inspect_conf_file('mods', $f); } return; } sub visit_control_files { my ($self, $item) = @_; return unless $self->is_apache2_related; return unless $item->is_maintainer_script; # skip anything but shell scripts return unless $item->is_shell_script; return unless $item->is_open_ok; open(my $sfd, '<', $item->unpacked_path) or die encode_utf8('Cannot open ' . $item->unpacked_path); my $position = 1; while (my $line = <$sfd>) { # skip comments next if $line =~ /^ [#]/x; # Do not allow reverse dependencies to call "a2enmod" and friends # directly if ($line =~ m{ \b (a2(?:en|dis)(?:conf|site|mod)) \b }x) { my $command = $1; $self->pointed_hint( 'apache2-reverse-dependency-calls-wrapper-script', $item->pointer($position), $command); } # Do not allow reverse dependencies to call "invoke-rc.d apache2 $self->pointed_hint('apache2-reverse-dependency-calls-invoke-rc.d', $item->pointer($position)) if $line =~ /invoke-rc\.d\s+apache2/; # XXX: Check whether apache2-maintscript-helper is used # unconditionally e.g. not protected by a [ -e ], [ -x ] or so. # That's going to be complicated. Or not possible without grammar # parser. } continue { ++$position; } return; } sub inspect_conf_file { my ($self, $conftype, $item) = @_; # Don't follow unsafe links return unless $item->is_open_ok; open(my $fd, '<', $item->unpacked_path) or die encode_utf8('Cannot open ' . $item->unpacked_path); my $skip = 0; my $position = 1; while (my $line = <$fd>) { ++$skip if $line =~ m{<\s*IfModule.*!\s*mod_authz_core} || $line =~ m{<\s*IfVersion\s+<\s*2\.3}; for my $directive ('Order', 'Satisfy', 'Allow', 'Deny', qr{}xsm, qr{}xsm) { if ($line =~ m{\A \s* ($directive) (?:\s+|\Z)}xsm && !$skip) { $self->pointed_hint('apache2-deprecated-auth-config', $item->pointer($position), $1); } } if ($line =~ /^#\s*(Depends|Conflicts):\s+(.*?)\s*$/) { my ($field, $value) = ($1, $2); $self->pointed_hint('apache2-unsupported-dependency', $item->pointer($position), $field) if $field eq 'Conflicts' && $conftype ne 'mods'; my @dependencies = split(/[\n\s]+/, $value); for my $dep (@dependencies) { $self->pointed_hint('apache2-unparsable-dependency', $item->pointer($position), $dep) if $dep =~ /[^\w\.]/ || $dep =~ /^mod\_/ || $dep =~ /\.(?:conf|load)/; } } --$skip if $line =~ m{<\s*/\s*If(Module|Version)}; } continue { ++$position; } close $fd; return; } 1; # Local Variables: # indent-tabs-mode: nil # cperl-indent-level: 4 # End: # vim: syntax=perl sw=4 sts=4 sr et