How to add headers in nginx only sometimes - nginx

I have a nginx proxy to a API server. The API sometimes sets the cache control header. If the API hasnt set the cache control I want nginx to override it.
How do I do that?
I think I want to do something like this, but it doesnt work.
location /api {
if ($sent_http_cache_control !~* "max-age=90") {
add_header Cache-Control no-store;
add_header Cache-Control no-cache;
add_header Cache-Control private;
}
proxy_pass $apiPath;
}

You cannot use if here, because if, being a part of the rewrite module, is evaluated at a very early stage of the request processing, way before proxy_pass is called and the header is returned from the upstream server.
One way to solve your problem is to use map directive. Variables defined with map are evaluated only when they are used, which is exactly what you need here. Sketchily, your configuration in this case would look like this:
# When the $custom_cache_control variable is being addressed
# look up the value of the Cache-Control header held in
# the $upstream_http_cache_control variable
map $upstream_http_cache_control $custom_cache_control {
# Set the $custom_cache_control variable with the original
# response header from the upstream server if it consists
# of at least one character (. is a regular expression)
"~." $upstream_http_cache_control;
# Otherwise set it with this value
default "no-store, no-cache, private";
}
server {
...
location /api {
proxy_pass $apiPath;
# Prevent sending the original response header to the client
# in order to avoid unnecessary duplication
proxy_hide_header Cache-Control;
# Evaluate and send the right header
add_header Cache-Control $custom_cache_control;
}
...
}

Awswer from Ivan Tsirulev is correct but you don't have to use regex.
Nginx uses the first parameter of map as default value automatically so you don't have to add that either.
# Get value from Http-Cache-Control header but override it when it's empty
map $upstream_http_cache_control $custom_cache_control {
'' "no-store, no-cache, private";
}
server {
...
location /api {
# Use the value from map
add_header Cache-Control $custom_cache_control;
}
...
}

Related

How do I set default headers for upstream responses in nginx?

I want to make sure that at least the default header value is always returned from upstream. Even if upstream is unavailable, which causes error 5xx.
Now I have tried these nginx config options:
server {
...
#add_header "Access-Control-Allow-Origin" "*"; №0
#add_header "Access-Control-Allow-Origin" "*" always; №1
#more_set_headers "Access-Control-Allow-Origin: *"; №2
#more_set_headers -s '403 500 502 504 503' "Access-Control-Allow-Origin: *"; №3
location /upstream {
proxy_pass http://localhost:1234;
}
...
}
There are problems with all the options:
№0: Duplicates the header, and in the case of 5xx will not return any.
№1: Duplicates the header
№2: Overrides the upstream header
№3: If the upstream ended with a good http code, but did not return a header, it will not add a header.
I think I'm close to the right solution, but I can't find it.
The map below uses a regex, /.+/, to check if the Access-Control-Allow-Origin header is defined. If so, it assigns its value to the $acao custom variable. Otherwise, it assigns the default value * to $acao;
To avoid duplications, use proxy_hide_header
Finally, add the header using the $acao variable content.
http {
map $upstream_http_access_control_allow_origin $acao {
~.+ $upstream_http_access_control_allow_origin;
default '*';
}
server {
#…
proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin $acao always;
location /upstream {
proxy_pass http://localhost:1234;
}
}

How to use the response header from an upstream host but also provide a default value?

I have an nginx conf which I pair with various applications to act as a reverse proxy.
I would like to enforce some specific response headers but allow the upstream service the freedom to override them.
If the inheritance rules for nginx were different, I would do something like
if ($sent_http_x_content_length = '') {
add_header X-Content-Length "nosniff" always;
}
if ($sent_http_content_type = '') {
add_header Content-Type "text/html" always;
}
But of course, since it's inside the if, the previous if blocks are overwritten.
What is a good workaround that doesn't involve installing the headers-more module?
Ultimately I went with the nginx-plus officially sanctioned more_set_headers community module.
map $sent_http_x_powered_by $resp_x_powered_by {
default $sent_http_x_powered_by;
"" "x powered by is empty";
}
map $sent_http_x_foo $resp_x_foo {
default "x foo by is set";
"" "x foo by is empty";
}
server {
add_header "SomeHeader" "SomeValue";
...
location / {
more_set_headers "X-Powered-By: $resp_x_powered_by";
more_set_headers X-Foo $resp_x_foo;
}
How this works:
more_set_headers will overwrite the headers that you specify with the value provided, e.g. $resp_x_powered_by
The map block will evaluate $resp_x_powered_by by looking at the string in $sent_http_x_powered_by.
If the string is "" then we use the default value of "x powered by is empty". Otherwise, we use the value from the upstream server.
Why I did this:
Initially I tried to use add_headers with map as Raul suggested, but the issue lies in header duplication. If the header is set, then add_header will simply add another line to define the header again. A common workaround is to use proxy_hide_header but that blanks out the value so when we hit the map block, it will always evaluate to "".
As you can see, there is an add_header block outside of the location block which would be overridden by scope inheritance. There are ways to get around it but more_set_headers solves that issue.

Nginx passing url arguments as header

In the following example:
http {
server { # simple reverse-proxy
listen 8080;
location / {
set $token $arg_token;
#return 200 $token;
add_header test "test $token";
proxy_pass http://localhost:5601;
}
} ...
}
if I leave return 200 $token I obtain the token as response + in header (which is a normal behavior) but when I delete return I obtain only "test" as test header value, what am I missing please ?
The proxy_set_header sets header that NGINX will use while communicating to the upstream/backend.
You won't see that added header in the response of NGINX back to the client.
If you want to see it, use add_header as well.

nginx config to enable CORS with origin matching

I've tried to use a very popular config for nginx, which enables CORS and supports origin matching using regular expressions.
Here's my config:
server {
listen 80 default_server;
root /var/www;
location / {
if ($http_origin ~ '^http://(www\.)?example.com$') {
add_header Access-Control-Allow-Origin "$http_origin";
}
# Handling preflight requests
if ($request_method = OPTIONS) {
add_header Content-Type text/plain;
add_header Content-Length 0;
return 204;
}
}
}
However, this config must use two conditions: one to match the origin domain name and another one to capture preflight requests. So when the second condition is matched, the headers from the first conditions are not added to the response.
According to the If Is Evil official article, this is an expected behavior for nginx.
If If Is Evil how do I enable CORS in nginx then? Or maybe there is a way to overcome this limitation somehow?
You can try to use map istead of the first if block:
map $http_origin $allow_origin {
~^http://(www\.)?example.com$ $http_origin;
}
map $http_origin $allow_methods {
~^http://(www\.)?example.com$ "OPTIONS, HEAD, GET";
}
server {
listen 80 default_server;
root /var/www;
location / {
add_header Access-Control-Allow-Origin $allow_origin;
add_header Access-Control-Allow-Methods $allow_methods;
# Handling preflight requests
if ($request_method = OPTIONS) {
add_header Content-Type text/plain;
add_header Content-Length 0;
return 204;
}
}
}
nginx will refuse to add an empty HTTP headers, so they will be added only if Origin header is present in request and matched this regex.
The only solution I've found so far is a hack to use a variable to aggregate multiple conditions and then match it with only a single if statement, therefore duplicating some directives:
server {
listen 80 default_server;
root /var/www;
location / {
set $cors '';
set $cors_allowed_methods 'OPTIONS, HEAD, GET';
if ($http_origin ~ '^https?://(www\.)?example.com$') {
set $cors 'origin_matched';
}
# Preflight requests
if ($request_method = OPTIONS) {
set $cors '${cors} & preflight';
}
if ($cors = 'origin_matched') {
add_header Access-Control-Allow-Origin $http_origin;
}
if ($cors = 'origin_matched & preflight') {
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Methods $cors_allowed_methods;
add_header Content-Type text/plain;
add_header Content-Length 0;
return 204;
}
}
}
A more compliant solution is a bit more involved but does de-duplicate the regex for domain matching, and can be placed into snippets.
I created the file /etc/nginx/snippets/cors-maps.conf which must be included inside the http { ... } block. It contains rules like so:
# always set value to append to Vary if Origin is set
map $http_origin $cors_site_v
{
~. 'Origin';
}
# set site-specific origin header if it matches our domain
map $http_origin $cors_site_origin
{
'~^https://(?:[-a-z\d]+\.)+example\.com$' $http_origin;
}
# validate the options only if domain matched
map '$request_method#$cors_site_origin#$http_access_control_request_method' $cors_site_options
{
# is an allowed method
'~^OPTIONS#.+#(?:GET|HEAD|POST|OPTIONS)$' okay;
# requested an unknown/disallowed method
'~^OPTIONS#.' nope;
}
# set value of Access-Control-Allow-Origin only if domain matched
map '$request_method#$cors_site_origin' $cors_site_acao
{
'~^(?:GET|HEAD|POST)#.' $cors_site_origin;
}
# set value of Access-Control-Allow-Credentials only if Origin was allowed
map $cors_site_acao $cors_site_acac
{
~. 'true';
}
Then /etc/nginx/snippets/cors-site.conf which can be included inside multiple location { ... } blocks:
# only using "if" safely with a "return" as explained in https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/
# return early without access headers for invalid pre-flight, because origin matched domain
if ($cors_site_options = nope)
{
add_header Vary $cors_site_v;
return 204 '';
}
# return early with access headers for valid pre-flight
if ($cors_site_options = okay)
{
add_header Access-Control-Allow-Origin $cors_site_origin;
add_header Access-Control-Allow-Credentials $cors_site_acac;
add_header Vary $cors_site_v;
add_header Access-Control-Allow-Methods 'GET, HEAD, POST, OPTIONS';
# probably overkill, gleaned from others' examples
add_header Access-Control-Allow-Headers 'Accept, Accept-Language, Authorization, Cache-Control, Content-Language, Content-Type, Cookie, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Mx-ReqToken, X-Requested-With';
add_header Access-Control-Max-Age 1728000;
return 204 '';
}
# conditionally set headers on actual requests, without "if", because directive ignored when values are empty strings ("map" default)
add_header Access-Control-Allow-Origin $cors_site_acao;
add_header Access-Control-Allow-Credentials $cors_site_acac;
add_header Vary $cors_site_v;
The # in the values to match aren't special, they simply serve as separators to allow tests with multiple input variables. Extra domains can be added to the map for $cors_site_origin, but would need a bit of tweaking to support domains with different allowed options/headers.
Without getting into the details of your nginx setup, it's not going to work anyway, because the CORS header's you're returning are incorrect...
Specifically:
For preflight (OPTIONS) requests, the following are the only meaningful CORS response headers: Access-Control-Allow Origin, (required), Access-Control-Allow Credentials (optional), Access-Control-Allow-Methods, (required), Access-Control-Allow-Headers, (required) and Access-Control-Max-Age, (optional). Any others are ignored.
For regular (non-OPTIONS) requests, the following are the only meaningful CORS response headers: Access-Control-Allow Origin (required), Access-Control-Allow Credentials (optional) and Access-Control-Expose-Headers (optional). Any others are ignored.
Note those required headers for pre-flight requests - currently you're only passing two of them... Also, note that you don't need to return Access-Control-Allow-Methods for a non-OPTIONS request - it's not 'valid', so will be ignored.
As far as your specific nginx issue goes, I think #Slava Fomin II has the correct-est answer...

nginx reject request if header not present

I need nginx to reject requests if header StaticCookie is not present. I don't care about its value, I just need for it to exist.
What I came up with is this, but this doesn't work. Nginx allows requests with no headers at all.
if ($http_StaticCookie = false) {
return 403;
}
root /usr/share/asuno/www;
location ~* /css/ {
expires max;
}
location ~* /js/ {
expires max;
}
I saw this post -
Nginx: Reject request if header is not present or wrong - but it deals with defined header values. What I need is to check mere existence of the header.
I tried putting location directives inside the if clause but then nginx throws errors trying to read the config.
How can this be done?
the comment by Alexey Ten is correct, thanks.
if ($http_StaticCookie = "") { return 403; }

Resources