| 1 | package Perlbal::Plugin::Redirect; |
|---|
| 2 | use strict; |
|---|
| 3 | use warnings; |
|---|
| 4 | |
|---|
| 5 | sub handle_request { |
|---|
| 6 | my ($svc, $pb) = @_; |
|---|
| 7 | |
|---|
| 8 | my $mappings = $svc->{extra_config}{_redirect_host}; |
|---|
| 9 | my $req_header = $pb->{req_headers}; |
|---|
| 10 | |
|---|
| 11 | # returns 1 if done with client, 0 if no action taken |
|---|
| 12 | my $map_using = sub { |
|---|
| 13 | my ($match_on) = @_; |
|---|
| 14 | |
|---|
| 15 | my $target_host = $mappings->{$match_on}; |
|---|
| 16 | |
|---|
| 17 | return 0 unless $target_host; |
|---|
| 18 | |
|---|
| 19 | my $path = $req_header->request_uri; |
|---|
| 20 | |
|---|
| 21 | my $res_header = Perlbal::HTTPHeaders->new_response(301); |
|---|
| 22 | $res_header->header('Location' => "http://$target_host$path"); |
|---|
| 23 | $res_header->header('Content-Length' => 0); |
|---|
| 24 | # For some reason a follow-up request gets a "400 Bad request" response, |
|---|
| 25 | # so until someone has time to figure out why, just punt and disable |
|---|
| 26 | # keep-alives after this request. |
|---|
| 27 | $res_header->header('Connection' => 'close'); |
|---|
| 28 | $pb->write($res_header->to_string_ref()); |
|---|
| 29 | |
|---|
| 30 | return 1; |
|---|
| 31 | }; |
|---|
| 32 | |
|---|
| 33 | # The following is lifted wholesale from the vhosts plugin. |
|---|
| 34 | # FIXME: Factor it out to a utility function, I guess? |
|---|
| 35 | # |
|---|
| 36 | # foo.site.com should match: |
|---|
| 37 | # foo.site.com |
|---|
| 38 | # *.foo.site.com |
|---|
| 39 | # *.site.com |
|---|
| 40 | # *.com |
|---|
| 41 | # * |
|---|
| 42 | |
|---|
| 43 | my $vhost = lc($req_header->header("Host")); |
|---|
| 44 | |
|---|
| 45 | # if no vhost, just try the * mapping |
|---|
| 46 | return $map_using->("*") unless $vhost; |
|---|
| 47 | |
|---|
| 48 | # Strip off the :portnumber, if any |
|---|
| 49 | $vhost =~ s/:\d+$//; |
|---|
| 50 | |
|---|
| 51 | # try the literal mapping |
|---|
| 52 | return 1 if $map_using->($vhost); |
|---|
| 53 | |
|---|
| 54 | # and now try wildcard mappings, removing one part of the domain |
|---|
| 55 | # at a time until we find something, or end up at "*" |
|---|
| 56 | |
|---|
| 57 | # first wildcard, prepending the "*." |
|---|
| 58 | my $wild = "*.$vhost"; |
|---|
| 59 | return 1 if $map_using->($wild); |
|---|
| 60 | |
|---|
| 61 | # now peel away subdomains |
|---|
| 62 | while ($wild =~ s/^\*\.[\w\-\_]+/*/) { |
|---|
| 63 | return 1 if $map_using->($wild); |
|---|
| 64 | } |
|---|
| 65 | |
|---|
| 66 | # last option: use the "*" wildcard |
|---|
| 67 | return $map_using->("*"); |
|---|
| 68 | } |
|---|
| 69 | |
|---|
| 70 | sub register { |
|---|
| 71 | my ($class, $svc) = @_; |
|---|
| 72 | |
|---|
| 73 | $svc->register_hook('Redirect', 'start_http_request', sub { handle_request($svc, $_[0]); }); |
|---|
| 74 | } |
|---|
| 75 | |
|---|
| 76 | sub unregister { |
|---|
| 77 | my ($class, $svc) = @_; |
|---|
| 78 | $svc->unregister_hooks('Redirect'); |
|---|
| 79 | } |
|---|
| 80 | |
|---|
| 81 | sub handle_redirect_command { |
|---|
| 82 | my $mc = shift->parse(qr/^redirect\s+host\s+(\S+)\s+(\S+)$/, "usage: REDIRECT HOST <match_host> <target_host>"); |
|---|
| 83 | my ($match_host, $target_host) = $mc->args; |
|---|
| 84 | |
|---|
| 85 | my $svcname; |
|---|
| 86 | unless ($svcname ||= $mc->{ctx}{last_created}) { |
|---|
| 87 | return $mc->err("No service name in context from CREATE SERVICE <name> or USE <service_name>"); |
|---|
| 88 | } |
|---|
| 89 | |
|---|
| 90 | my $svc = Perlbal->service($svcname); |
|---|
| 91 | return $mc->err("Non-existent service '$svcname'") unless $svc; |
|---|
| 92 | |
|---|
| 93 | $svc->{extra_config}{_redirect_host} ||= {}; |
|---|
| 94 | $svc->{extra_config}{_redirect_host}{lc($match_host)} = lc($target_host); |
|---|
| 95 | |
|---|
| 96 | return 1; |
|---|
| 97 | } |
|---|
| 98 | |
|---|
| 99 | # called when we are loaded |
|---|
| 100 | sub load { |
|---|
| 101 | Perlbal::register_global_hook('manage_command.redirect', \&handle_redirect_command); |
|---|
| 102 | |
|---|
| 103 | return 1; |
|---|
| 104 | } |
|---|
| 105 | |
|---|
| 106 | # called for a global unload |
|---|
| 107 | sub unload { |
|---|
| 108 | return 1; |
|---|
| 109 | } |
|---|
| 110 | |
|---|
| 111 | 1; |
|---|
| 112 | |
|---|
| 113 | =head1 NAME |
|---|
| 114 | |
|---|
| 115 | Perlbal::Plugin::Redirect - Plugin to do redirecting in Perlbal land |
|---|
| 116 | |
|---|
| 117 | =head1 SYNOPSIS |
|---|
| 118 | |
|---|
| 119 | LOAD redirect |
|---|
| 120 | |
|---|
| 121 | CREATE SERVICE redirector |
|---|
| 122 | SET role = web_server |
|---|
| 123 | SET plugins = redirect |
|---|
| 124 | REDIRECT HOST example.com www.example.net |
|---|
| 125 | ENABLE redirector |
|---|
| 126 | |
|---|
| 127 | =head1 LIMITATIONS |
|---|
| 128 | |
|---|
| 129 | Right now this can only redirect at the hostname level. Also, it just |
|---|
| 130 | assumes you want an http: URL. |
|---|