Reputation: 11760
This came up from time here and there but no question properly covers this use case.
The relevant section of http
:
map $http_origin $origin_with_default {
default '*';
~. $http_origin;
}
map $request_method $es_target {
default '';
POST 'search';
GET 'search';
HEAD 'search';
OPTIONS 'options';
}
root /app;
The relevant section of server
:
server {
location ~* /(.*)/_search {
limit_except OPTIONS {
auth_basic "Read Users";
auth_basic_user_file /etc/nginx/htpasswd_read;
}
rewrite ^ /internal/$es_target;
}
location /internal {
return 405;
}
location /internal/search/ {
internal;
proxy_pass http://elasticsearch/;
proxy_http_version 1.1;
proxy_set_header Connection "Keep-Alive";
proxy_set_header Proxy-Connection "Keep-Alive";
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
include "cors.headers";
}
location /internal/options {
internal;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
add_header 'Access-Control-Max-Age' 1728000;
include "cors.headers";
return 204;
}
}
Finally, the cors.headers
file:
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Origin $origin_with_default always;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always;
add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
POST https://example.com/index_name/_search
gives 401. This is as expected.OPTIONS https://example.com/index_name/_search
returns the options headers. This is also as expected.POST https://u:[email protected]/index_name/_search
gives a 404 and the server log contains open() "/app/index_name/_search" failed (2: No such file or directory),
. Before I added the rewrite ^ /internal/$es_target;
and the location /internal/search/
section, when the proxy_pass
was just after limit_except
inside location ~* /(.*)/_search {
it did work. Because of 1) and 2) I believe the rewrite and the location matching works. But why does it try to serve a file instead of doing a proxy pass?Upvotes: 2
Views: 1759
Reputation: 15478
The following answer will require the minimal understanding of two nginx base concepts - the request processing phases (described in the development guide) and the location content handler (which can be either local, when the request is served using some local file contents, or not, when the response is coming from some kind of upstream - an HTTP proxy, FastCGI or uWSGI daemon, etc.).
Despite the fact that I have quite extensive experience in configuring nginx, limit_except
isn't a directive I'd used to use frequently. To understand its behavior I did a couple of tests. Here is the list of nginx directives I'm going to use and the request processing phases where they are registering their handlers, in order of execution:
rewrite
- NGX_HTTP_REWRITE_PHASE
auth_basic
- NGX_HTTP_ACCESS_PHASE
try_files
- NGX_HTTP_PRECONTENT_PHASE
proxy_pass
- NGX_HTTP_CONTENT_PHASE
From all the above directives only auth_basic
and proxy_pass
are allowed to use inside the limit_except
block. The try_files "" <location>
trick I'm going to use described in this answer at ServerFault, so I would skip its detailed description here.
TL;DR The solution will be provided at the next part of the answer; the limit_except
directive cannot be used to solve the problem.
limit_except
directive behavior analysisI will use the following config to analyze the limit_except
directive behavior:
server {
listen 8080;
return 200 "upstream: request URI is \"$request_uri\", request method is $request_method";
}
server {
listen 80;
root /var/www/html;
index index.html;
... locations will vary during the tests
}
Under the /var/www/html
directory I'll place a single index.html
file with the single text line index
.
Here we go.
location / {
limit_except GET {
proxy_pass http://127.0.0.1:8080;
}
}
> curl http://127.0.0.1/
index
> curl -X POST http://127.0.0.1/
upstream: request URI is "/", request method is POST
For the GET
request nginx uses the local content handler. For the POST
request nginx uses the http_proxy_module
content handler.
location / {
limit_except GET {}
proxy_pass http://127.0.0.1:8080;
}
> curl http://127.0.0.1/
upstream: request URI is "/", request method is GET
> curl -X POST http://127.0.0.1/
upstream: request URI is "/", request method is POST
Here nginx uses the defined http_proxy_module
content handler for both requests. We didn't find anything we can't be expect yet. Lets go further.
location / {
rewrite ^ /internal;
limit_except GET {}
proxy_pass http://127.0.0.1:8080;
}
location /internal {
return 200 internal;
}
> curl http://127.0.0.1/
internal
> curl -X POST http://127.0.0.1/
upstream: request URI is "/", request method is POST
The rewrite
rules are completely ignored if the request falls under the limit_except
condition. This looks like something we did not expect. However a quick search gives us the nginx trac ticket referring the following comment:
The problem that rewrite module directives (
set
,if
) are not inherited into thelimit_except
block, and not executed there. ... This behaviour is basically identical to a nestedlocation
block.
Now let's check the try_files
directive behavior. To do it we will add the following map
block
map $request_method $loc_name {
POST pst;
default def;
}
and two named locations
location @def { return 200 def; }
location @pst { return 200 pst; }
to our configuration.
location / {
try_files "" @$loc_name;
}
> curl http://127.0.0.1/
def
> curl -X POST http://127.0.0.1/
pst
The unconditional jump to the named location works as expected.
location / {
proxy_pass http://127.0.0.1:8080;
try_files "" @$loc_name;
}
> curl http://127.0.0.1/
def
> curl -X POST http://127.0.0.1/
pst
This is also expected, since the NGX_HTTP_PRECONTENT_PHASE
where try_files
attaches its handler is executed before the NGX_HTTP_CONTENT_PHASE
one.
location / {
limit_except GET {}
try_files "" @$loc_name;
}
> curl http://127.0.0.1/
def
> curl -X POST http://127.0.0.1/
(HTTP 405 Not Allowed)
Looks like the nginx tries to use local content handler for the POST
request.
location / {
limit_except GET {}
proxy_pass http://127.0.0.1:8080;
try_files "" @$loc_name;
}
> curl http://127.0.0.1/
def
> curl -X POST http://127.0.0.1/
upstream: request URI is "/", request method is POST
Bad news. The NGX_HTTP_PRECONTENT_PHASE
handler defined in the main location did not get executed if the request falls under the limit_except
condition. This looks similar to the nested locations behavior, although in contradistinction to the nested location we can't use the try_files
directive inside the limit_except
block.
Looks like the limit_except
directive has some kind of limited use cases. Does it mean the question problem is not solvable? No. It means the limit_except
directive cannot be used to solve it and we need to find some other way. Never give up :)
You can optionally enable/disable basic authentication using the technique I just described here. Add the additional map
block to your configuration:
map $es_target $realm {
search "Read Users";
default off;
}
Now you can enable conditional basic auth in a following way:
location ~ /_search$ {
auth_basic $realm;
auth_basic_user_file /etc/nginx/htpasswd_read;
}
However you can't use the rewrite
directive in this block since that directive is executed during the NGX_HTTP_REWRITE_PHASE
, and auth_basic
directive register its handler at the later NGX_HTTP_ACCESS_PHASE
. While for the regular allow
/deny
directives there is a way to do all the checks using only the rewrite module directives (generic example is here), there is no such a way for basic auth. Fortunately we still can use our try_files
trick (which will be executed at the later NGX_HTTP_PRECONTENT_PHASE
). If by chance you are using OpenResty bundle or lua-nginx-module, you have an additional options described in the aforementioned answer.
I can see you already faced the problems with the correct URI that should be passed to the upstream. Your original proxy_pass http://elasticsearch/;
will pass the /
for every request, and the proxy_pass http://elasticsearch;
will pass the rewritten URI. While your original request URI is always available via the $request_uri
variable (which does not get changed with the rewrite
directive unlike the $uri
one), and something like proxy_pass http://elasticsearch$request_uri;
should work too, we will use the named locations (we are not limited to, but that way we should prevent any URI changes at all). Here is the whole solution (I slightly optimize your first map
block to prevent (some kind of expensive) regex library call):
map $http_origin $origin_with_default {
'' '*';
default $http_origin;
}
map $request_method $es_target {
POST 'search';
GET 'search';
HEAD 'search';
OPTIONS 'options';
default 'wrong';
}
map $es_target $realm {
search "Read Users";
default off;
}
location ~ /_search$ {
auth_basic $realm;
auth_basic_user_file /etc/nginx/htpasswd_read;
try_files "" @$es_target;
}
location @search {
proxy_pass http://elasticsearch;
proxy_http_version 1.1;
proxy_set_header Connection "Keep-Alive";
proxy_set_header Proxy-Connection "Keep-Alive";
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
include "cors.headers";
}
location @options {
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
add_header 'Access-Control-Max-Age' 1728000;
include "cors.headers";
return 204;
}
location @wrong {
return 405;
}
Upvotes: 4
Reputation: 11760
Here's a working config. It needed limit_except
removed, every location switched to regex matching consuming everything -- and the last location being last is important to be so otherwise it gets into a rewrite loop. The "If the proxy_pass directive is specified with a URI, then when a request is passed to the server, the part of a normalized request URI matching the location is replaced by a URI specified in the directive" part of the rewrite module is not something I was able to get working.
We still need the http
section:
map $http_origin $origin_with_default {
default '*';
~. $http_origin;
}
map $request_method $es_target {
default 'invalid';
POST 'search';
GET 'search';
HEAD 'search';
OPTIONS 'options';
}
And then comes server
server {
location ~ /internal/search/(?<search>.*) {
internal;
auth_basic "Read Users";
auth_basic_user_file /etc/nginx/htpasswd_read;
proxy_pass http://elasticsearch/$search;
proxy_http_version 1.1;
proxy_set_header Connection "Keep-Alive";
proxy_set_header Proxy-Connection "Keep-Alive";
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
include "cors.headers";
}
location ~ /internal/options {
internal;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
add_header 'Access-Control-Max-Age' 1728000;
include "cors.headers";
return 204;
}
location ~ /internal/invalid {
return 405;
}
location ~* /_search$ {
rewrite (.*) /internal/$es_target$1;
}
}
Upvotes: 0