| Apache-Gateway documentation | Contained in the Apache-Gateway distribution. |
Apache::Gateway - A Bloated Gateway Module
Example Apache configuration:
<Location /CPAN/> SetHandler perl-script PerlHandler Apache::Gateway PerlSetVar GatewayConfig /etc/apache/gateway/CPAN PerlSetupEnv Off </Location>
Example GatewayConfig file:
GatewayRoot /CPAN/ <LocationMatch ".*"> Site ftp://ftp.perl.org/pub/perl/CPAN/ MuxSite ftp://ftp.cdrom.com/pub/perl/CPAN/ MuxSite ftp://ftp.digital.com/pub/plan/perl/CPAN/ Site ftp://ftp.orst.edu/pub/packages/CPAN/ Site ftp://ftp.funet.fi/pub/languages/perl/CPAN/ </LocationMatch> ClockBroken ftp://ftp.cdrom.com EET PST8PDT ClockBroken ftp://ftp.digital.com EET PST8PDT ClockBroken ftp://ftp.orst.edu EET PST8PDT ClockBroken ftp://ftp.perl.org CST CST6CDT
See the examples directory for commented examples.
The Apache::Gateway module implements a gateway with assorted
optional features.
Apache::Gateway services requests using LWP and, hence, can
gateway to any protocol that LWP understands. It also makes
foreign URIs appear to be local URIs.
Apache::Gateway does not include a cache, but it can be used in
combination with a proxy cache to cache what the gateway retrieves.
For example, Apache can provide caching for the gateway by setting
up a proxy cache virtual host and a gateway virtual host and then
using the proxy to access the gateway.
Multiple mirrors can provide an instance. Requests which fail will automatically be retried with the next mirror. This capability is very useful when some mirrors are busy or erratic.
Like the CPAN multiplexer, Apache::Gateway can multiplex requests
amongst several mirrors.
The origin server to contact can vary depending upon the URL. This capability is particularly useful when dealing with partial mirrors. A common situation is that some files may be available at all mirrors, but less commonly used files will only be available at a few mirrors.
(Need to think of a better name for this feature.) Remote FTP directory listings can be modified to refer to the gateway. This feature is somewhat similar to the ProxyPassReverse directive.
This feature was especially complicated and problematical. It has now been removed.
Apache::Gateway can try to correct incorrect timestamps generated
by popular mirroring software. In particular, it can try to
compensate for the way the Perl mirror program sets timestamps.
Most configuration is done in the GatewayConfig files. The regular
Apache configuration files only need to include the handler
directives and set the GatewayConfig filename. Environment
variables are not used, so PerlSetupEnv can be Off.
GatewayConfig directives purposely look like Apache config directives so that the syntax will be familiar. However, GatewayConfig directives are not Apache config directives. They cannot be used in Apache config files (and vice versa)!
Sets the root of the gatewayed area on the local server. Generally
matches the Location setting in the Apache config files.
Defaults to "/".
Passes timeout (in seconds) to LWP::UserAgent.
Begins a LocationMatch section. Works similarly to the ApacheLocation
match directive except that the pattern is a Perl regexp. Note: there
are no Location or other style sections, only LocationMatch.
LocationMatch sections are tried in order until a regexp is matched.
Sets an upstream server to contact for this URI. In case of failure, requests are automatically retried with successive sites in the order they appear. Failures can include anything from the upstream server being down or flaky to a file not being present because the upstream mirror is out of synch with its primary site.
Sets an upstream server to contact for this URI. Adjacent MuxSite
servers are tried in round robin order.
For example, here again is the default portion of the sample GatewayConfig file above:
<LocationMatch ".*"> Site ftp://ftp.perl.org/pub/perl/CPAN/ MuxSite ftp://ftp.cdrom.com/pub/perl/CPAN/ MuxSite ftp://ftp.digital.com/pub/plan/perl/CPAN/ Site ftp://ftp.orst.edu/pub/packages/CPAN/ Site ftp://ftp.funet.fi/pub/languages/perl/CPAN/ </LocationMatch>
With the Site and MuxSite directives here, the first request
will be forwarded to ftp.perl.org. If it fails, the request will be
retried with cdrom, digital, orst, and funet, in that order. The
next request for that process will be tried with ftp.perl.org first
again. If it fails, retries then go to digital, cdrom, orst, and
finally funet.
A good general strategy for packages with multiple mirrors might be to specify one or two nearby sites to try first. Then specify some multiplexed sites slightly further away in case the nearby sites fail. Finally, fall back to the primary site if all else fails.
When caching is employed and requests can be gatewayed to multiple mirrors, timestamp correctness becomes more important. Unfortunately, timestamps on mirrored files are usually wrong. For example, the popular Perl mirror program is generally configured to match timestamps using the local timezone both locally and on the server it is mirroring. This strategy is only guaranteed to work if both servers are in the same timezone.
Example: ClockBroken ftp://ftp.cdrom.com EET PST8PDT
cdrom gets files from funet, which seems always to use the EET timezone (which is two hours off from GMT) for purposes of mirroring. cdrom, however, uses the PST8PDT timezone, so that 00:00 on funet differs from 00:00 on cdrom by 9 or 10 hours, depending upon whether or not Daylight Savings Time is in effect. The example ClockBroken line corrects for this disparity.
Note: timezones are those understood by Time::Zone.
The following internal functions are documented (mostly useful for hackers):
Construct a new Apache::Gateway object describing a gateway. If a LWP::UserAgent is not provided, a new one will be created. Note: the user agent is modified for seach request; it is not constant and is probably not shareable.
Get/set the user agent.
Get/set the Apache request currently being gatewayed. To send the request, see the send_request method.
Get/set the configuration information for this gateway location. Can be overridden to provide dynamic per location information
Clear request headers in $r in preparation for a redirect.
Return semicanonicalized server URL (without trailing slash).
Return the (somewhat canonicalized) "server name" portion of the URL. The "server name" is defined as the leading scheme://authority portion of the URL.
Return the (somewhat canonicalized) "server name" portion of the URL of this server. The "server name" is defined as the leading scheme://authority portion of the URL. Currently assumes server access is via HTTP.
Get the usual time difference (in seconds) between the two time zones. Will yield the wrong results in the midst of a change to/from daylight savings time. Specifically, as used in this module, this function will return the wrong results when applied to files retrieved by the mirror during the two hours of the year when one server is in Daylight Savings Time and the other is not.
Update Via header in HTTP::Response with information about this hop.
Hop information combines protocol information from the message with
server information from the Apache server. The server name
returned is hardcoded as 'apache'.
Eventually, options should be provided to control hostname suppression and comment customization.
Copy the headers from an HTTP::Headers object to an
Apache::Request. Hope that the Apache request object will later
print out the headers in "Good Practice" order (there appears to be no
way of controlling this).
The only tricky item is the Content-Type header, which needs special handling.
Try a redirect. We do this via LWP::UserAgent because
internal_redirect_handler does not provide hooks for detecting and
recovering from errors.
Get/set the site tried. Can be used to determine which upstream server actually fields a request.
Try the site $gw->site. Ideally, we could use
Apache::internal_redirect_handler to try the redirects. However,
it provides no hook for detecting an error and aborting output.
That's not mod_perl's fault--Apache source would need to be
modified to support such a hook.
Try sites in order until one succeeds. $allow_last_site_abort indicates if the last site can/should be aborted after examing the head for its error code. All other sites always allow premature abortion.
Abortion is needed because only one request can be allowed to run to completion and produce a message body.
Get the list of sites to try for this request. Can be overridden to customize the list of sites to try.
By default, this method looks through the LocationMatch sections in the GatewayConfig file in order and returns the sites in the first section matched.
Send the Apache request to the upstream server. Optionally sets it first.
Apache::Gateway is a big, complicated module that loads many other
modules. As such, it pushes mod_perl to its limits, especially
when used with DSO/APXS.
The current version of LWP (5.35) only supports If-Modified-Since
for file and ftp URLs. Thus, gatewaying to ftp servers will actually
be better than gatewaying to http servers for cached responses.
A ProxyRemote-like capability is needed for origin servers which must be accessed through a proxy.
A ProxyPassReverse analogue might be useful, too.
Apache::Gateway assumes it is being accessed using HTTP. Ought to
handle cases where this gateway is accessed using https (SSL).
There is no way to tell LWP to use a proxy.
The Server response header field should contain information about
the origin server, not this server. Unfortunately, Apache overrides
any existing origin server information in this field.
Charles C. Fu, perl@web-i18n.net
perl(1), Apache(3pm), LWP(3pm).
| Apache-Gateway documentation | Contained in the Apache-Gateway distribution. |
package Apache::Gateway;
use strict; use vars qw(@ISA); use Exporter (); @ISA = qw(Exporter); $Apache::Gateway::VERSION = sprintf("%d.%02d", q$Revision: 1.12 $ =~ /(\d+)\.(\d+)/g); use Apache::Constants ':server'; # for SERVER_VERSION for Via comment use Apache::URI (); use HTTP::Date (); use HTTP::Request (); use HTTP::Status (); use IO::File (); use LWP::UserAgent (); use Time::Zone (); # In an Apache::Registry script, we would need to make the following # variables global. However, making them global seems unnecesary in a # handler. my %default_port = (finger => 79, ftp => 21, gopher => 70, http => 80, https => 443, nntp => 119, prospero => 1525, rlogin => 513, snews => 563, telnet => 23, wais => 210, webster => 765, whois => 43, ); my $gw;
sub new($;$) { my $proto = shift; my $class = ref($proto) || $proto; my $self = {}; $self->{UA} = @_ ? shift : new LWP::UserAgent, $self->{CONFIG} = {}; bless($self, $class); return $self; }
sub user_agent($;$) { my $self = shift; if (@_) { $self->{UA} = shift } return $self->{UA}; }
sub request($;$) { my $self = shift; if (@_) { $self->{REQUEST} = shift } return $self->{REQUEST}; } # $gw->_config( [$config] ) # Get/set the cached configuration information and current run state. # This very low-level method is for hackers only. This API might # change. sub _config($;$) { my $self = shift; if (@_) { $self->{CONFIG} = shift } return $self->{CONFIG}; }
sub location_config($;$) { my $self = shift; my $config_file = $self->{REQUEST}->dir_config('GatewayConfig'); if (@_) { $self->{CONFIG}{$config_file} = shift } return $self->{CONFIG}{$config_file}; } # $gw->_init_config_file # # If necessary, parse and cache a configuration file specified by the # GatewayConfig variable. On error, sets # $r->status(HTTP::Status::RC_INTERNAL_SERVER_ERROR) and returns. # Parsed info and state is stored in $gw->_config, which has the # following structure # $gw->_config->{$config_filename} # = { LOCATION => [ { PATTERN => Regexp, # SITE => [ site or mux sites list, ... ] }, # ... ], # BROKEN_CLOCK => { $server0 => [upstream^2 TZ, upstream TZ], # $server1 => [upstream^2 TZ, upstream TZ], # ... } # ROOT => location of root of gateway, # TIMEOUT => timeout in seconds for contacting upstream server # } # site = a site URL, e.g., http://www.perl.com/CPAN/ # mux sites list = { START_INDEX => start index of round robin, # SITE => [ site, site, ... ] } # This structure is subject to change. Because it contains state # information, it is per object and cannot be shared. sub _init_config_file($) { my $self = shift; my $r = $self->{REQUEST}; my $config = $self->{CONFIG}; my $config_file = $r->dir_config('GatewayConfig'); unless ($config_file) { $r->log_error('no GatewayConfig'); $r->status(HTTP::Status::RC_INTERNAL_SERVER_ERROR); return; } # Return if file has already been parsed. return 1 if exists $config->{$config_file}; # Open file. my $f = IO::File->new($config_file, 'r'); unless ($f) { $r->log_error('open ' . $config_file . ': ' . $!); $r->status(HTTP::Status::RC_INTERNAL_SERVER_ERROR); return; } my $gw_root = '/'; my($timeout, @entry, %clock_broken); # Read lines. while(<$f>) { s/\s+$//; # Remove trailing whitespace. next if /^$/ || /\#/; # Ignore blank and comment lines. # E.g., <LocationMatch "\.gz$"> if(/^<LocationMatch \s*\"(.*)\">$/) { # Begin a new entry. my $cur_entry = { PATTERN => $1, SITE => [] }; while(<$f>) { s/\s+$//; # Remove trailing whitespace. next if /^$/ || /\#/; # Ignore blank and comment lines. if(/Site/) { my $site = $cur_entry->{SITE}; # E.g., Site http://www.perl.com/CPAN/ if(/^Site\s*(.*)/) { # Add one or more sites to this entry. push @$site, split(' ', $1); } elsif(/^MuxSite\s*(.*)/) { # Add one or more muliplexed sites to this entry. # E.g., MuxSite http://www.perl.com/CPAN/ my $last_site_added = $site->[$#$site]; if(ref($last_site_added)) { push @{$last_site_added->{SITE}}, split(' ', $1); } else { # start_index = start index of round robin push @$site, { START_INDEX => 0, SITE => [ split(' ', $1) ] }; } } } elsif(/^<\/LocationMatch>$/) { push @entry, $cur_entry; last; } else { $r->log_error('Unrecognized command: ' . $_); $r->status(HTTP::Status::RC_INTERNAL_SERVER_ERROR); return; } } } elsif(/^ClockBroken\s*(.*)/) { # E.g., ClockBroken ftp://ftp.fuller.edu EST5EDT PST8PDT Yes my($server, @arg) = split(' ', $1); $arg[1] = 'GMT' unless defined $arg[1]; $clock_broken{$server} = [ @arg ]; } elsif(/^GatewayRoot\s*(.*)/) { $gw_root = $1; } elsif(/^GatewayTimeout\s*(.*)/) { $timeout = $1; } else { $r->log_error('Unrecognized command: ' . $_); $r->status(HTTP::Status::RC_INTERNAL_SERVER_ERROR); return; } } # Store parsed results. $config->{$config_file} = { LOCATION => \@entry, ROOT => $gw_root, BROKEN_CLOCK => \%clock_broken }; $config->{$config_file}{TIMEOUT} = $timeout; return 1; }
sub clear_headers_for_redirect($) { my $r = shift; # Some of this should be done with Apache::Tie when it is working. $r->header_out('Content-Length' => undef); # should use tie $r->status(HTTP::Status::RC_OK); my %err = $r->err_headers_out; # should use tie foreach (keys %err) { $r->err_header_out($_ => undef); } }
sub canonicalized_server_URL($$$) { my($scheme, $host, $port) = @_; my $server = lc($scheme . '://' . $host); if(defined $port and exists $default_port{$scheme} and $port != $default_port{$scheme}) { $server .= ':' . $port; } return $server; }
sub server_name_from_URL($$) { my ($r, $url) = @_; $url = Apache::URI->parse($r, $url) unless ref $url; return canonicalized_server_URL($url->scheme, $url->hostname, $url->port); }
sub server_name($) { my $r = shift; return canonicalized_server_URL('http', $r->server->server_hostname, $r->server->port); }
sub diff_TZ($$) { my($mirror_TZ, $origin_TZ) = @_; return 0 if $origin_TZ eq $mirror_TZ; # no need to do anything # Use Thu Jan 01 00:00:00 GMT 1998 as a reference time. No # changes to/from Daylight Savings Time occurred near this time. my $reference_time = 883612800; return Time::Zone::tz_offset(Time::Zone::tz2zone($mirror_TZ), $reference_time) - Time::Zone::tz_offset(Time::Zone::tz2zone($origin_TZ), $reference_time); }
sub update_via_header_field($$) { my($self, $response) = @_; my $r = $self->{REQUEST}; # Set protocol. my $hop = $response->protocol; # Oops. No protocol. Try to guess from request. unless(defined $hop) { $hop = (uc(Apache::URI->parse($r, $response->request->url)->scheme) . '/unknown'); } # HTTP protocol-name can be dropped. Remember if the server is # being accessed via HTTP. my $server_accessed_via_HTTP = ($hop =~ s{^HTTP/}{}); # Set server name. $hop .= ' apache'; # For now, use a pseudonym. $hop .= ':' . $r->server->port unless $server_accessed_via_HTTP && $r->server->port == 80; # Set comment. Comment text may not contain embedded parentheses. my $comment = SERVER_VERSION; $comment =~ tr/()/[]/; # Replace parentheses with brackets. $hop .= ' (' . $comment . ')'; # Append comment. # Update header. my $via = $response->header('Via'); $response->header(Via => defined $via ? $via . ', ' . $hop : $hop); }
sub copy_header_to_Apache_request($$) { my($r, $header) = @_; # Apache might already know the proper content type, e.g., by use # of a ForceType directive. If so, try not to override it. Else, # the type needs to be set explicitly with the Apache request's # content_type method: simply setting the header value isn't # enough. if(defined $r->content_type) { $header->content_type(undef); } else { $r->content_type($header->content_type); } # Copy headers to Apache request (in "Good Practice" order). $header->scan(sub {$r->header_out(@_);}); } sub print_headers($$$) { my ($self, $response, $allow_abort) = @_; my $r = $self->{REQUEST}; my $site = $self->{SITE}; my $path = $self->{GW_PATH}; # Copy status code and reason phrase from response to Apache # request. $r->status($1) if $response->status_line =~ /^(\d+)/; $r->status_line($response->status_line); # Attempt to abort on failure. return if $allow_abort && $response->is_error; # $r->log_error('Gateway: ' . $response->request->url # . ' ' . $response->status_line); # configuration info for this directory my $loc_conf = $self->location_config; # Try to modify Content-Base to refer to our multiplexer. if(my $base = $response->header('Content-Base')) { # where site appears on gateway, e.g., <http://www.perl.com/CPAN/>. my $gw_site = server_name($r) . $loc_conf->{ROOT}; $response->header(Content_Base => $base) if $base =~ s/^$site/$gw_site/; } # If necessary, try to compensate for servers with broken clocks. if(my $lm = $response->last_modified) { my $upstream_server = server_name_from_URL($r, $response->request->url->as_string); if(exists $loc_conf->{BROKEN_CLOCK}{$upstream_server}) { my $TZ = $loc_conf->{BROKEN_CLOCK}{$upstream_server}; $response->last_modified($lm + diff_TZ($$TZ[1], $$TZ[0])); } } $self->update_via_header_field($response); copy_header_to_Apache_request($r, $response); $r->send_http_header; }
sub redirect($$) { my ($self, $allow_abort) = @_; my $r = $self->{REQUEST}; my $ua = $self->{UA}; my $site = $self->{SITE}; my $path = $self->{GW_PATH}; my $url = Apache::URI->parse($r, $site . $path); # If this is an anon-FTP request, fill in the password with the # UA's from field. if($url->scheme eq 'ftp' && $url->user eq 'anonymous') { $url->password($ua->from) # anon-FTP passwd } my $request = HTTP::Request->new($r->method, $url->unparse); # If upstream server has a broken clock, calculate how much we # need to adjust condition GET time fields. Note: this code won't # work correctly if we get redirected to another server with a # different clock. Oh, well. my $loc_conf = $self->location_config; my $upstream_server = server_name_from_URL($r, $url); my $broken_clock = 0; if(exists $loc_conf->{BROKEN_CLOCK}{$upstream_server}) { my $TZ = $loc_conf->{BROKEN_CLOCK}{$upstream_server}; $broken_clock = diff_TZ($$TZ[1], $$TZ[0]); } if(my $IMS = $r->header_in('If-Modified-Since')) { $request->if_modified_since(HTTP::Date::str2time($IMS) - $broken_clock); } if(my $IUmS = $r->header_in('If-Unmodified-Since')) { $request->if_unmodified_since(HTTP::Date::str2time($IUmS) - $broken_clock); } $request->header(Accept => $r->header_in('Accept')); # Pragma directives must be passed through. if(my $pragma = $r->header_in('Pragma')) { $request->header($pragma); $request->header('Cache-Control' => 'no-cache') # HTTP/1.1 if $pragma =~ /^no-cache$/i; } # Cache directives must be passed through. if(my $cache = $r->header_in('Cache-Control')) { $request->header('Cache-Control' => $cache); } # We would like the first callback to occur as soon as reasonably # possible after the headers have been retrieved. Thus, we need a # small size argument because the first callback may not occur # until all the headers plus size bytes of the content have been # retrieved. my $headers_printed; my $response = $ua->request($request, sub { my($data, $response) = @_; $self->print_headers($response, $allow_abort) unless $headers_printed; $headers_printed = 1; return if($allow_abort && $response->is_error || $r->connection->aborted); $r->print($data); }, 1024); # Be sure we've printed the headers. We need this check here # because callback will never get called for responses with no # content. $self->print_headers($response, $allow_abort) unless $headers_printed || $r->connection->aborted; }
sub site($;$) { my $self = shift; if (@_) { $self->{SITE} = shift } return $self->{SITE}; }
sub try_URI($$) { my ($self, $allow_abort) = @_; clear_headers_for_redirect($self->{REQUEST}); $self->redirect($allow_abort); }
sub try_sites($$@) { my ($self, $allow_last_site_abort, @site) = @_; my $r = $self->{REQUEST}; # Try all but last site, aborting each attempt on error. for(my $i = 0; $i <= $#site; ++$i) { if(ref $site[$i]) { # Try this group of sites, starting at index $idx. my $mux_site = $site[$i]; my $idx = $mux_site->{START_INDEX}; my $list = $mux_site->{SITE}; my $last = $#$list; $self->try_sites($i < $#site || $allow_last_site_abort, @$list[$idx .. $last], @$list[0 .. ($idx - 1)]); # Increment index for next time round. $mux_site->{START_INDEX} = $idx < $last ? ++$idx : 0; } else { $self->{SITE} = $site[$i]; $self->try_URI($i < $#site || $allow_last_site_abort); } # We can exit if the last attempt succeeded or if the client # is no longer talking to us. return if(!HTTP::Status::is_error($r->status) || $r->connection->aborted); } } # Set up the user agent for this particular request. sub _init_ua($) { my $self = shift; my $r = $self->{REQUEST}; my $ua = $self->{UA}; $ua->from($r->server->server_admin); $ua->agent($r->header_in('User-Agent')); $ua->timeout($self->location_config->{TIMEOUT}); return 1; # succeeded } # Set $self->{GW_PATH} to the portion of the path relative to # GatewayRoot. This is also the path which is appended to the URIs of # the upstream servers. sub _init_path($) { my $self = shift; my $r = $self->{REQUEST}; # epath = $gw_root . $gw_path my $gw_root = $self->location_config->{ROOT}; my ($gw_path) = $r->parsed_uri->path =~ /^\Q$gw_root\E(.*)/; unless(defined $gw_path) { # error $r->log_error($r->uri . ' does not begin with ' . $gw_root); $r->status(HTTP::Status::RC_INTERNAL_SERVER_ERROR); return; } $self->{GW_PATH} = $gw_path; # succeeded return 1; } sub _init_request($) { my $self = shift; $self->_init_config_file or return; $self->_init_ua or return; $self->_init_path or return; return 1; # succeeded }
sub site_list($) { my $self = shift; my $location_conf = $self->location_config; my $gw_path = $self->{GW_PATH}; foreach my $entry (@{$location_conf->{LOCATION}}) { if($gw_path =~ /$entry->{PATTERN}/) { return @{$entry->{SITE}}; } } return; }
sub send_request($;$) { my $self = shift; if (@_) { $self->{REQUEST} = shift } $self->_init_request or return; $self->try_sites(0, $self->site_list); return 1; # succeeded } sub handler { if(! defined $gw) { $gw = new Apache::Gateway; } $gw->send_request(shift); return 0; } 1; __END__