|
| 1 | +package Kelp::Middleware; |
| 2 | + |
| 3 | +use Kelp::Base; |
| 4 | +use Plack::Util; |
| 5 | +use Kelp::Util; |
| 6 | +use Carp; |
| 7 | + |
| 8 | +attr -app => sub { croak 'app is required' }; |
| 9 | + |
| 10 | +sub _wrap_basic |
| 11 | +{ |
| 12 | + my ($self, $psgi, $middleware) = @_; |
| 13 | + |
| 14 | + for my $class (@$middleware) { |
| 15 | + |
| 16 | + # Make sure the middleware was not already loaded |
| 17 | + # This does not apply for testing, in which case we want |
| 18 | + # the middleware to wrap every single time |
| 19 | + next if $self->{_loaded_middleware}->{$class}++ && !$ENV{KELP_TESTING}; |
| 20 | + |
| 21 | + my $mw = Plack::Util::load_class($class, 'Plack::Middleware'); |
| 22 | + my $args = $self->app->config("middleware_init.$class") // {}; |
| 23 | + |
| 24 | + Kelp::Util::_DEBUG(modules => "Wrapping app in $mw middleware with args: ", $args); |
| 25 | + |
| 26 | + $psgi = $mw->wrap($psgi, %$args); |
| 27 | + } |
| 28 | + |
| 29 | + return $psgi; |
| 30 | +} |
| 31 | + |
| 32 | +sub _wrap_advanced |
| 33 | +{ |
| 34 | + my ($self, $psgi, $middleware) = @_; |
| 35 | + |
| 36 | + for my $name (@$middleware) { |
| 37 | + |
| 38 | + # Make sure the middleware was not already loaded |
| 39 | + # This does not apply for testing, in which case we want |
| 40 | + # the middleware to wrap every single time |
| 41 | + next if $self->{_loaded_middleware}->{$name}++ && !$ENV{KELP_TESTING}; |
| 42 | + |
| 43 | + my $config = $self->app->config("middleware_init.$name") // {}; |
| 44 | + next if $config->{disabled}; |
| 45 | + my $path = $config->{path} // ''; |
| 46 | + my $class = $config->{class}; |
| 47 | + my $args = $config->{args} // {}; |
| 48 | + |
| 49 | + # forced first slash, no trailing slash |
| 50 | + $path =~ s{^/? (.*?) /?$}{/$1}x; |
| 51 | + |
| 52 | + Kelp::Util::_DEBUG(modules => "Wrapping app in $name middleware (under path $path) with args: ", $args); |
| 53 | + my $mw = Plack::Util::load_class($class, 'Plack::Middleware'); |
| 54 | + my $wrapped = $mw->wrap($psgi, %$args); |
| 55 | + |
| 56 | + if ($path eq '/') { |
| 57 | + |
| 58 | + # no need to wrap again if path is root |
| 59 | + $psgi = $wrapped; |
| 60 | + } |
| 61 | + else { |
| 62 | + my $orig_psgi = $psgi; |
| 63 | + my $prepared_path = qr{^ \Q$path\E (/|$)}x; |
| 64 | + |
| 65 | + # wrap to check PATH_INFO |
| 66 | + $psgi = sub { |
| 67 | + goto $wrapped if $_[0]->{PATH_INFO} =~ $prepared_path; |
| 68 | + goto $orig_psgi; |
| 69 | + }; |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + return $psgi; |
| 74 | +} |
| 75 | + |
| 76 | +sub wrap |
| 77 | +{ |
| 78 | + my ($self, $psgi) = @_; |
| 79 | + |
| 80 | + if (defined(my $middleware = $self->app->config('middleware'))) { |
| 81 | + my $type = $self->app->config('middleware_type', 'basic'); |
| 82 | + my $method = "_wrap_$type"; |
| 83 | + $psgi = $self->$method($psgi, $middleware); |
| 84 | + } |
| 85 | + |
| 86 | + return $psgi; |
| 87 | +} |
| 88 | + |
| 89 | +1; |
| 90 | + |
| 91 | +__END__ |
| 92 | +
|
| 93 | +=pod |
| 94 | +
|
| 95 | +=head1 NAME |
| 96 | +
|
| 97 | +Kelp::Middleware - Kelp app wrapper (PSGI middleware) |
| 98 | +
|
| 99 | +=head1 SYNOPSIS |
| 100 | +
|
| 101 | + middleware => [qw(TrailingSlashKiller Static)], |
| 102 | + middleware_init => { |
| 103 | + TrailingSlashKiller => { |
| 104 | + redirect => 1, |
| 105 | + }, |
| 106 | + Static => { |
| 107 | + path => qr{^/static}, |
| 108 | + root => '.', |
| 109 | + }, |
| 110 | + } |
| 111 | +
|
| 112 | +=head1 DESCRIPTION |
| 113 | +
|
| 114 | +This is a small helper object which wraps Kelp in PSGI middleware. It is loaded |
| 115 | +and constructed by Kelp based on the value of L<Kelp/middleware_obj> (class |
| 116 | +name). |
| 117 | +
|
| 118 | +=head2 Middleware types |
| 119 | +
|
| 120 | +Kelp has a couple of middleware wrapper types available. Default type is |
| 121 | +C<basic>, which is straightforward to configure (same as Kelp modules). |
| 122 | +C<advanced> is more verbose in configuration, but adds additional capabilities. |
| 123 | +To change the type of middleware wrapper you use, specify C<middleware_type> |
| 124 | +configuration key. |
| 125 | +
|
| 126 | +=head3 Basic |
| 127 | +
|
| 128 | +Default configuration, same as shown in L</SYNOPSIS> and L<Kelp::Manual/Adding |
| 129 | +middleware>. |
| 130 | +
|
| 131 | +=head3 Advanced |
| 132 | +
|
| 133 | +More advanced configuration, adds some helpful capabilities: |
| 134 | +
|
| 135 | +=over |
| 136 | +
|
| 137 | +=item * multiple middlewares of the same type |
| 138 | +
|
| 139 | +You must specify your own names for the middlewares, and each name have its own |
| 140 | +C<class> attached to it. This way you can easily introduce more than one |
| 141 | +middleware of the same type. C<class> can be specified the same way as normally |
| 142 | +- either a submodule of C<Plack::Middleware::> namespace, or full namespace when |
| 143 | +prefixed by C<+>. |
| 144 | +
|
| 145 | +=item * disabling middlewares |
| 146 | +
|
| 147 | +It's hard to disable middleware in environment-specific config (like |
| 148 | +C<deployment>), and it's impossible to choose the order of new middleware added |
| 149 | +in these configs (they will always come last). |
| 150 | +
|
| 151 | +To solve this, you can set C<< disabled => 1 >> in your main config, and |
| 152 | +override with C<< disabled => 0 >> in environmental configs. |
| 153 | +
|
| 154 | +=item * choosing a path for middlewares |
| 155 | +
|
| 156 | +Middlewares let you skip a ton of coding, but with basic config you can only |
| 157 | +use them at the root level. This may be a pain if you only want a middleware |
| 158 | +used under a specific path. |
| 159 | +
|
| 160 | +With advanced middleware, you can specify a C<path>. Your app will only run the |
| 161 | +middleware if the path matches (as a prefix). |
| 162 | +
|
| 163 | +=back |
| 164 | +
|
| 165 | +The arguments passed to the actual middleware must be wrapped inside the |
| 166 | +C<args> structure. Outside of that structure, you must specify C<class> and can |
| 167 | +specify C<path> and C<disabled>, as discussed above. |
| 168 | +
|
| 169 | + middleware_type => 'advanced', |
| 170 | + middleware => [qw(kill_slash public_files static_files api_auth)], |
| 171 | + middleware_init => { |
| 172 | + kill_slash => { |
| 173 | + class => 'TrailingSlashKiller, |
| 174 | + args => { |
| 175 | + redirect => 1, |
| 176 | + }, |
| 177 | + }, |
| 178 | + public_files => { |
| 179 | + class => 'Static', |
| 180 | + path => '/public', |
| 181 | + args => { |
| 182 | + root => '.', |
| 183 | + }, |
| 184 | + }, |
| 185 | + static_files => { |
| 186 | + class => 'Static', |
| 187 | + path => '/static', |
| 188 | + disabled => 1, |
| 189 | + args => { |
| 190 | + root => '.', |
| 191 | + }, |
| 192 | + }, |
| 193 | + api_auth => { |
| 194 | + class => 'Auth::Basic', |
| 195 | + path => '/api', |
| 196 | + args => { |
| 197 | + authenticator => app, |
| 198 | + }, |
| 199 | + }, |
| 200 | + }, |
| 201 | +
|
| 202 | +=head1 ATTRIBUTES |
| 203 | +
|
| 204 | +=head2 app |
| 205 | +
|
| 206 | +Main application object. Required. |
| 207 | +
|
| 208 | +=head1 METHODS |
| 209 | +
|
| 210 | +=head2 wrap |
| 211 | +
|
| 212 | + $wrapped_psgi = $object->wrap($psgi) |
| 213 | +
|
| 214 | +Wraps the object in all middlewares according to L</app> configuration. |
| 215 | +
|
0 commit comments