knadh / listmonk

High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app.
https://listmonk.app
GNU Affero General Public License v3.0
15.02k stars 1.37k forks source link

CORS issue, Listmonk configured with Nginx reverse proxy #1523

Closed krmjitsingh closed 1 year ago

krmjitsingh commented 1 year ago

I have configure listmonk with nginx reverse, getting cores error this issue is only with react with nodejs everything working fine

Access to fetch at 'https://listmonk.unikaksha.com/api/subscribers' from origin 'https://localhost:3000/' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled CORS

henk23 commented 1 year ago

Possibly related issue here in version 2.5.1. Nginx configuration has the correct headers:

add_header Access-Control-Allow-Origin $allow_origin always;
add_header Access-Control-Allow-Methods 'GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS' always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Headers 'Authorization, Content-Type' always;

But the OPTIONS request fails

❯ curl 'https://listmonk.my-website.com/api/public/subscription' -X OPTIONS
{"message":"Unauthorized"}
❯ curl 'https:/listmonk.my-website.com/api/public/subscription' -X OPTIONS -I
HTTP/1.1 401 Unauthorized
Server: nginx/1.18.0 (Ubuntu)
Date: Thu, 14 Sep 2023 08:57:37 GMT
Content-Type: application/json; charset=UTF-8
Content-Length: 27
Connection: keep-alive
Www-Authenticate: basic realm=Restricted
Access-Control-Allow-Methods: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Authorization, Content-Type
duraki commented 1 year ago

@krmjitsingh this will help you #1521 - just apply my patch on the fork and build listmonk again @henk23 again, not every reverse proxy allows for setting custom headers, we should find a mere modular way of it

henk23 commented 1 year ago

Hey, sorry @duraki I have no idea what you're referring to, there seems to be a history that I don't know.

Just saying: Configuring the reverse proxy correctly will lead to Listmonk throwing a 401 at an OPTIONS request, that should be a 2xx. Right?

duraki commented 1 year ago

@henk23 Take a look at #1521 - it explains this issue and more similar issues on the listmonk repo. In short, I'm using a reverse proxy which doesn't provide setting up a custom CORS allowed headers and configurations, and listmonk doesn't allow setting these CORS stuff on the application layer (ie. in .env or similar config.toml et al.) therefore many users are greeted with similar errors in production. We should allow some kind of settings that allows CORS bypass or setting up origin header correctly and then spew this in the HTTP req/resp pipeline - a la what my gitpatch does on the referenced issue.

Kindly.

knadh commented 1 year ago

Ah, I think I know what's going on. For the public API to work, the Enable public subscription page option in General settings have to be turned on.

henk23 commented 1 year ago

Hi @knadh, sorry that's not it. The option is turned on and I still get the 401 response. A curl POST request on that endpoint works without a problem, but an OPTIONS request fails with 401. Seems like this needs to be handled separately. Don't know anything about Golang, so can't really chime in to @duraki's suggestion

krmjitsingh commented 1 year ago

Hello I am able to fix issue by adding this in listmonk nginx configuration file

#
# Wide-open CORS config for nginx
#
location / {
     if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '*';
        #
        # Om nom nom cookies
        #
        add_header 'Access-Control-Allow-Credentials' 'true';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        #
        # Custom headers and headers various browsers *should* be OK with but aren't
        #
        add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
        #
        # Tell client that this pre-flight info is valid for 20 days
        #
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Content-Length' 0;
        return 204;
     }
     if ($request_method = 'POST') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Credentials' 'true';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
     }
     if ($request_method = 'GET') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Credentials' 'true';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
     }
}
knadh commented 1 year ago

I think this is the ideal approach for now, to add CORS support in the proxy layer. If this were to be inside listmonk, then it'd require special configuration for support domain names.

That said:

add_header 'Access-Control-Allow-Origin' '*';

You should change * to the domain from which you're making the request. Otherwise, you open up the end point to carpet bombing attacks from arbitrary domains!

codersgyan commented 2 weeks ago

In case you are using Cloudflare to proxy your requests, you can do following:

  1. Create a Cloudflare worker with following code:
export default {
  async fetch(request, env, ctx) {
    // Handle OPTIONS requests separately
    if (request.method === 'OPTIONS') {
      return new Response(null, {
        status: 200,
        headers: {
          'Access-Control-Allow-Origin': '*', // or specify your domain
          'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
          'Access-Control-Allow-Headers': 'Content-Type, Authorization',
          'Access-Control-Allow-Credentials': 'true'
        }
      });
    }

    // Forward all other requests to the original server
    const response = await fetch(request);

    // Clone the response to modify headers
    const newResponse = new Response(response.body, response);

    // Add CORS headers to the response
    newResponse.headers.set('Access-Control-Allow-Origin', '*'); // or better specify your domain
    newResponse.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    newResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    newResponse.headers.set('Access-Control-Allow-Credentials', 'true');

    return newResponse;
  }
}
  1. Create a worker route in Cloudflare:
// you can put whatever your domain is. I am using subdomain thats why put wildcard.
*.yourdomain.com/*

And assign the created worker to this route from select box options.

  1. Make sure you deploy the worker and your requests should works.

Happy coding 🎉