| 1 | ########################################################################### |
|---|
| 2 | # plugin to do name-based virtual hosts |
|---|
| 3 | ########################################################################### |
|---|
| 4 | |
|---|
| 5 | # things to test: |
|---|
| 6 | # one persistent connection, first to a docs plugin, then to web proxy... see if it returns us to our base class after end of request |
|---|
| 7 | # PUTing a large file to a selector, seeing if it is put correctly to the PUT-enabled web_server proxy |
|---|
| 8 | # obvious cases: non-existent domains, default domains (*), proper matching (foo.brad.lj before *.brad.lj) |
|---|
| 9 | # |
|---|
| 10 | |
|---|
| 11 | package Perlbal::Plugin::Vhosts; |
|---|
| 12 | |
|---|
| 13 | use strict; |
|---|
| 14 | use warnings; |
|---|
| 15 | no warnings qw(deprecated); |
|---|
| 16 | |
|---|
| 17 | our %Services; # service_name => $svc |
|---|
| 18 | |
|---|
| 19 | # when "LOAD" directive loads us up |
|---|
| 20 | sub load { |
|---|
| 21 | my $class = shift; |
|---|
| 22 | |
|---|
| 23 | Perlbal::register_global_hook('manage_command.vhost', sub { |
|---|
| 24 | my $mc = shift->parse(qr/^vhost\s+(?:(\w+)\s+)?(\S+)\s*=\s*(\w+)$/, |
|---|
| 25 | "usage: VHOST [<service>] <host_or_pattern> = <dest_service>"); |
|---|
| 26 | my ($selname, $host, $target) = $mc->args; |
|---|
| 27 | unless ($selname ||= $mc->{ctx}{last_created}) { |
|---|
| 28 | return $mc->err("omitted service name not implied from context"); |
|---|
| 29 | } |
|---|
| 30 | |
|---|
| 31 | my $ss = Perlbal->service($selname); |
|---|
| 32 | return $mc->err("Service '$selname' is not a selector service") |
|---|
| 33 | unless $ss && $ss->{role} eq "selector"; |
|---|
| 34 | |
|---|
| 35 | $host = lc $host; |
|---|
| 36 | return $mc->err("invalid host pattern: '$host'") |
|---|
| 37 | unless $host =~ /^[\w\-\_\.\*\;\:]+$/; |
|---|
| 38 | |
|---|
| 39 | $ss->{extra_config}->{_vhosts} ||= {}; |
|---|
| 40 | $ss->{extra_config}->{_vhosts}{$host} = $target; |
|---|
| 41 | |
|---|
| 42 | return $mc->ok; |
|---|
| 43 | }); |
|---|
| 44 | return 1; |
|---|
| 45 | } |
|---|
| 46 | |
|---|
| 47 | # unload our global commands, clear our service object |
|---|
| 48 | sub unload { |
|---|
| 49 | my $class = shift; |
|---|
| 50 | |
|---|
| 51 | Perlbal::unregister_global_hook('manage_command.vhost'); |
|---|
| 52 | unregister($class, $_) foreach (values %Services); |
|---|
| 53 | return 1; |
|---|
| 54 | } |
|---|
| 55 | |
|---|
| 56 | # called when we're being added to a service |
|---|
| 57 | sub register { |
|---|
| 58 | my ($class, $svc) = @_; |
|---|
| 59 | unless ($svc && $svc->{role} eq "selector") { |
|---|
| 60 | die "You can't load the vhost plugin on a service not of role selector.\n"; |
|---|
| 61 | } |
|---|
| 62 | |
|---|
| 63 | $svc->selector(\&vhost_selector); |
|---|
| 64 | $svc->{extra_config}->{_vhosts} = {}; |
|---|
| 65 | |
|---|
| 66 | $Services{"$svc"} = $svc; |
|---|
| 67 | return 1; |
|---|
| 68 | } |
|---|
| 69 | |
|---|
| 70 | # called when we're no longer active on a service |
|---|
| 71 | sub unregister { |
|---|
| 72 | my ($class, $svc) = @_; |
|---|
| 73 | $svc->selector(undef); |
|---|
| 74 | delete $Services{"$svc"}; |
|---|
| 75 | return 1; |
|---|
| 76 | } |
|---|
| 77 | |
|---|
| 78 | sub dumpconfig { |
|---|
| 79 | my ($class, $svc) = @_; |
|---|
| 80 | |
|---|
| 81 | my $vhosts = $svc->{extra_config}->{_vhosts}; |
|---|
| 82 | |
|---|
| 83 | return unless $vhosts; |
|---|
| 84 | |
|---|
| 85 | my @return; |
|---|
| 86 | |
|---|
| 87 | while (my ($vhost, $target) = each %$vhosts) { |
|---|
| 88 | push @return, "VHOST $vhost = $target"; |
|---|
| 89 | } |
|---|
| 90 | |
|---|
| 91 | return @return; |
|---|
| 92 | } |
|---|
| 93 | |
|---|
| 94 | # call back from Service via ClientHTTPBase's event_read calling service->select_new_service(Perlbal::ClientHTTPBase) |
|---|
| 95 | sub vhost_selector { |
|---|
| 96 | my Perlbal::ClientHTTPBase $cb = shift; |
|---|
| 97 | |
|---|
| 98 | my $req = $cb->{req_headers}; |
|---|
| 99 | return $cb->_simple_response(404, "Not Found (no reqheaders)") unless $req; |
|---|
| 100 | |
|---|
| 101 | my $vhost = $req->header("Host"); |
|---|
| 102 | |
|---|
| 103 | # Browsers and the Apache API considers 'www.example.com.' == 'www.example.com' |
|---|
| 104 | $vhost and $vhost =~ s/\.$//; |
|---|
| 105 | |
|---|
| 106 | my $uri = $req->request_uri; |
|---|
| 107 | my $maps = $cb->{service}{extra_config}{_vhosts} ||= {}; |
|---|
| 108 | |
|---|
| 109 | # ability to ask for one host, but actually use another. (for |
|---|
| 110 | # circumventing javascript/java/browser host restrictions when you |
|---|
| 111 | # actually control two domains). |
|---|
| 112 | if ($vhost && $uri =~ m!^/__using/([\w\.]+)(?:/\w+)(?:\?.*)?$!) { |
|---|
| 113 | my $alt_host = $1; |
|---|
| 114 | |
|---|
| 115 | # update our request object's Host header, if we ended up switching them |
|---|
| 116 | # around with /__using/... |
|---|
| 117 | my $svc_name = $maps->{"$vhost;using:$alt_host"}; |
|---|
| 118 | my $svc = $svc_name ? Perlbal->service($svc_name) : undef; |
|---|
| 119 | unless ($svc) { |
|---|
| 120 | $cb->_simple_response(404, "Vhost twiddling not configured for requested pair."); |
|---|
| 121 | return 1; |
|---|
| 122 | } |
|---|
| 123 | |
|---|
| 124 | $req->header("Host", $alt_host); |
|---|
| 125 | $svc->adopt_base_client($cb); |
|---|
| 126 | return 1; |
|---|
| 127 | } |
|---|
| 128 | |
|---|
| 129 | # returns 1 if done with client, 0 if no action taken |
|---|
| 130 | my $map_using = sub { |
|---|
| 131 | my ($match_on, $force) = @_; |
|---|
| 132 | |
|---|
| 133 | my $map_name = $maps->{$match_on}; |
|---|
| 134 | my $svc = $map_name ? Perlbal->service($map_name) : undef; |
|---|
| 135 | |
|---|
| 136 | return 0 unless $svc || $force; |
|---|
| 137 | |
|---|
| 138 | unless ($svc) { |
|---|
| 139 | $cb->_simple_response(404, "Not Found (no configured vhost)"); |
|---|
| 140 | return 1; |
|---|
| 141 | } |
|---|
| 142 | |
|---|
| 143 | $svc->adopt_base_client($cb); |
|---|
| 144 | return 1; |
|---|
| 145 | }; |
|---|
| 146 | |
|---|
| 147 | # foo.site.com should match: |
|---|
| 148 | # foo.site.com |
|---|
| 149 | # *.foo.site.com -- this one's questionable, but might as well? |
|---|
| 150 | # *.site.com |
|---|
| 151 | # *.com |
|---|
| 152 | # * |
|---|
| 153 | |
|---|
| 154 | # if no vhost, just try the * mapping |
|---|
| 155 | return $map_using->("*", 1) unless $vhost; |
|---|
| 156 | |
|---|
| 157 | # Strip off the :portnumber, if any |
|---|
| 158 | $vhost =~ s/:\d+$//; |
|---|
| 159 | |
|---|
| 160 | # try the literal mapping |
|---|
| 161 | return if $map_using->($vhost); |
|---|
| 162 | |
|---|
| 163 | # and now try wildcard mappings, removing one part of the domain |
|---|
| 164 | # at a time until we find something, or end up at "*" |
|---|
| 165 | |
|---|
| 166 | # first wildcard, prepending the "*." |
|---|
| 167 | my $wild = "*.$vhost"; |
|---|
| 168 | return if $map_using->($wild); |
|---|
| 169 | |
|---|
| 170 | # now peel away subdomains |
|---|
| 171 | while ($wild =~ s/^\*\.[\w\-\_]+/*/) { |
|---|
| 172 | return if $map_using->($wild); |
|---|
| 173 | } |
|---|
| 174 | |
|---|
| 175 | # last option: use the "*" wildcard |
|---|
| 176 | return $map_using->("*", 1); |
|---|
| 177 | } |
|---|
| 178 | |
|---|
| 179 | 1; |
|---|