Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle CORS headers and OPTIONS method for HTTP API #623

Closed
routerino opened this issue Jun 9, 2022 · 20 comments
Closed

Handle CORS headers and OPTIONS method for HTTP API #623

routerino opened this issue Jun 9, 2022 · 20 comments
Labels
bug Something isn't working stale

Comments

@routerino
Copy link

routerino commented Jun 9, 2022

Bug description

When trying to use a browser to generate API requests (like, hypothetically, if you're building a web frontend for headscale), the browser expects to to use CORS to determine if it can talk to the external server. The browser does this by the following:

  • Sending a pre-flight OPTIONS request, expecting back a 204 response with the CORS headers attached
  • Once the response is accepted, sending the real API request with all the data

For this to work, we need two things:

  • The server (headscale) to generate CORS headers (and have it be configurable to set the domains appropriately)
  • The server to accept OPTIONS requests without authorization.

To Reproduce
Generate a fetch request from a browser in a separate domain. Such as:

let apiKey = '<my api key>';
let url = 'https://<my headscale domain>/api/v1/machine';

window.fetch(url, {
    method: 'GET',
    headers: {
        Authorization: `Bearer ${apiKey}`
    }}).then((resp) => resp.json()).then(function (data) {console.log(data);}).catch(function (error) {console.log(error);});});

If no CORS headers are specified, you get this nice error in the browser console:
image

If you have the right headers (if you, for example, inject them with a reverse proxy) but the OPTIONS request is blocked by authorization, you get this nice error instead:
image

Because the OPTIONS request is returning a 401 unauthorized when it shouldn't.

Both are not ideal. You can fix both with a reverse proxy, but you certainly shouldn't have to. The web server (gin?) should return OPTIONS with a 204 and be setting the CORS headers on all requests (and the CORS headers should be configurable).

Context info

These problems were fixed externally by routing through a Caddy reverse proxy using these matching settings:

@hs-options {
	host hs.<my-domain>
	method OPTIONS
}
@hs-other {
	host hs.<my-domain>
}
handle @hs-options {
	header {
		Access-Control-Allow-Origin https://<my-frontend-subdomain>
		Access-Control-Allow-Headers *
	}
	respond 204
}
handle @hs-other {
	reverse_proxy http://headscale:8080 {
		header_down Access-Control-Allow-Origin https://<my-frontend-subdomain>
		header_down Access-Control-Allow-Headers *
	}
}
@routerino routerino added the bug Something isn't working label Jun 9, 2022
@kradalby kradalby added this to the v0.17.0 milestone Jun 11, 2022
@kradalby kradalby modified the milestones: v0.17.0, v0.19.0 Oct 28, 2022
@Mikle-Bond
Copy link

If you use caddy-docker-proxy, here's the same (mostly) config, done in labels:

    labels:
      caddy: "headscale.${BASE_DOMAIN}"
      [email protected]: "headscale.${BASE_DOMAIN}"
      [email protected]: "headscale.${BASE_DOMAIN}"
      [email protected]: OPTIONS

      caddy.0_import: tlsdns

      caddy.1_handle: "@hs-options"
      caddy.1_handle.header.Access-Control-Allow-Origin: "https://ui.headscale.${BASE_DOMAIN}"
      caddy.1_handle.header.Access-Control-Allow-Headers: "*"
      caddy.1_handle.header.Access-Control-Allow-Methods: '"POST, GET, OPTIONS, DELETE"'
      caddy.1_handle.respond: "204"

      caddy.8_handle: /metrics
      caddy.8_handle.import: auth
      caddy.8_handle.reverse_proxy: "{{upstreams 9090}}"

      caddy.9_handle: "@hs-other"
      caddy.9_handle.reverse_proxy: "{{upstreams 80}}"
      caddy.9_handle.reverse_proxy.header_down_1: "Access-Control-Allow-Origin https://ui.headscale.${BASE_DOMAIN}"
      caddy.9_handle.reverse_proxy.header_down_2: "Access-Control-Allow-Headers *"
      caddy.9_handle.reverse_proxy.header_down_3: 'Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE"'

@deimjons
Copy link

deimjons commented Aug 30, 2023

Someone know how to configure it for Traefik?
I tryed to add next labels:

traefik.http.routers.headscale-public-https.middlewares: headscale-cors
traefik.http.middlewares.headscale-cors.headers.customResponseHeaders.Access-Control-Allow-Origin: https://web.headscale.yourdomain.example
traefik.http.middlewares.headscale-cors.headers.customResponseHeaders.Access-Control-Allow-Methods: GET, POST, PUT, DELETE
traefik.http.middlewares.headscale-cors.headers.customResponseHeaders.Access-Control-Allow-Headers: Content-Type

Looks like the first part of the problem is solved, but I have a problem with 204 status code in the answer..

I think maybe this plugin should help but I can't configure it properly, it shows me error "status code is smallest than minimum value: 100"
(Issue with details opened here: Medzoner/traefik-plugin-cors-preflight#8)

@Mikle-Bond
Copy link

@deimjons
Copy link

deimjons commented Sep 1, 2023

@Mikle-Bond Thank you for your attention. I tried this plugin but it didn't help me. I don't know: I doing something wrong or the plugin just not working.

I have added additional routes in labels:

      traefik.http.routers.headscale-options.rule: Host(`headscale.yourdomain.example/api/v1/apikey`) && Method(`OPTIONS`)
      traefik.http.routers.headscale-options.entrypoints: websecure
      traefik.http.routers.headscale-options.tls: true
      traefik.http.routers.headscale-options.tls.certresolver: prod
      traefik.http.routers.headscale-options.middlewares: replace-response-code@file  

also, I added a plugin and middleware (like they show in the documentation example) in the configuration file of traefik: traefik.yaml

experimental:
  plugins:
    traefik-replace-response-code:
      moduleName: "github.com/pierre-verhaeghe/traefik-replace-response-code"
      version: "v0.2.0"

http:
  middlewares:
    replace-response-code:
      plugin:
        traefik-replace-response-code:
          inputCode: 401
          outputCode: 200
          removeBody: "true"

As a result, I have the same error:

Access to fetch at 'https://headscale.yourdomain.example/api/v1/apikey' from origin 'https://admin.headscale.yourdomain.example' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

@mich2k
Copy link

mich2k commented Sep 20, 2023

Someone know how to configure it for Traefik? I tryed to add next labels:

traefik.http.routers.headscale-public-https.middlewares: headscale-cors
traefik.http.middlewares.headscale-cors.headers.customResponseHeaders.Access-Control-Allow-Origin: https://web.headscale.yourdomain.example
traefik.http.middlewares.headscale-cors.headers.customResponseHeaders.Access-Control-Allow-Methods: GET, POST, PUT, DELETE
traefik.http.middlewares.headscale-cors.headers.customResponseHeaders.Access-Control-Allow-Headers: Content-Type

Looks like the first part of the problem is solved, but I have a problem with 204 status code in the answer..

I think maybe this plugin should help but I can't configure it properly, it shows me error "status code is smallest than minimum value: 100" (Issue with details opened here: Medzoner/traefik-plugin-cors-preflight#8)

did you manage? I also have to do this

edit: https://doc.traefik.io/traefik/v2.4/middlewares/headers/

@deimjons
Copy link

deimjons commented Oct 1, 2023

no, I use it via prefix /admin.. ((

@kradalby kradalby removed this from the v0.19.0 milestone Dec 10, 2023
@masterwishx
Copy link

How to add this to Nginx Proxy Manager ?

@sapstar
Copy link

sapstar commented Feb 11, 2024

How to add this to Nginx Proxy Manager ?

Hi, did you ever figure this out? I am also unable to access api via NPM.

@masterwishx
Copy link

How to add this to Nginx Proxy Manager ?

Hi, did you ever figure this out? I am also unable to access api via NPM.

Yes, all working fine, if you using cloudflare disable the proxy (orange cloud)

@sapstar
Copy link

sapstar commented Feb 12, 2024

Thank you very much. That sorted it.

@fcwys
Copy link

fcwys commented Feb 29, 2024

I hope to support CORS, and I would like to use healscale directly instead of using Nginx and other programs for proxy, which is very inconvenient

@B08Z
Copy link

B08Z commented Feb 29, 2024

If you use caddy-docker-proxy, here's the same (mostly) config, done in labels:

    labels:
      caddy: "headscale.${BASE_DOMAIN}"
      [email protected]: "headscale.${BASE_DOMAIN}"
      [email protected]: "headscale.${BASE_DOMAIN}"
      [email protected]: OPTIONS

      caddy.0_import: tlsdns

      caddy.1_handle: "@hs-options"
      caddy.1_handle.header.Access-Control-Allow-Origin: "https://ui.headscale.${BASE_DOMAIN}"
      caddy.1_handle.header.Access-Control-Allow-Headers: "*"
      caddy.1_handle.header.Access-Control-Allow-Methods: '"POST, GET, OPTIONS, DELETE"'
      caddy.1_handle.respond: "204"

      caddy.8_handle: /metrics
      caddy.8_handle.import: auth
      caddy.8_handle.reverse_proxy: "{{upstreams 9090}}"

      caddy.9_handle: "@hs-other"
      caddy.9_handle.reverse_proxy: "{{upstreams 80}}"
      caddy.9_handle.reverse_proxy.header_down_1: "Access-Control-Allow-Origin https://ui.headscale.${BASE_DOMAIN}"
      caddy.9_handle.reverse_proxy.header_down_2: "Access-Control-Allow-Headers *"
      caddy.9_handle.reverse_proxy.header_down_3: 'Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE"'

Has anyone else made this work I can't figure it out.

@fcwys
Copy link

fcwys commented Feb 29, 2024

I don't use Caddy, and I don't actually have any plans to use it. I just want to run Headscale directly.

@B08Z
Copy link

B08Z commented Mar 4, 2024

Cross-Origin Request Warning: The Same Origin Policy will disallow reading the remote resource at https://headscale.domain.com/api/v1/node soon. (Reason: When the Access-Control-Allow-Headers is *, the Authorization header is not covered. To include the Authorization header, it must be explicitly listed in CORS header Access-Control-Allow-Headers).

I am getting this error with the above implementation using Headscale-admin

@GoodiesHQ
Copy link

@B08Z This might be super old and you may have opened an issue on headscale-admin referencing this, but you should be able to use this value to allow CORS from anywhere:

"Access-Control-Allow-Headers Authorization, *"
or perhaps
'Access-Control-Allow-Headers "Authorization, *"'
is the right syntax?

It needs to be explicit for whatever reason.

Copy link
Contributor

This issue is stale because it has been open for 90 days with no activity.

@github-actions github-actions bot added the stale label Jul 26, 2024
Copy link
Contributor

github-actions bot commented Aug 2, 2024

This issue was closed because it has been inactive for 14 days since being marked as stale.

@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Aug 2, 2024
@fjeddy
Copy link

fjeddy commented Aug 22, 2024

This issue is preventing and stopping developers from creating any serious web-ui for headscale and should not be closed, without this, all we're gonna have is the five minute UI's that currently exist.

@Corbeno
Copy link

Corbeno commented Aug 25, 2024

Here's my solution for Nginx Proxy Manager. I'm no expert but it works :)

I have two different URLs, for example:
headscale.mydomain.com
headscale-admin.mydomain.com

I put this under the NPM config for headscale.mydomain.com

location / {
        # This part isn't necessary for the CORS fix, just also what I  have
	proxy_pass $forward_scheme://$server:$port;
	proxy_http_version 1.1;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "upgrade";
	proxy_set_header Host $host;
	#proxy_redirect http:// https://; # don't work, not sure
	proxy_buffering off;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Forwarded-Proto $scheme;
	add_header Strict-Transport-Security "max-age=15552000; includeSubDomains";

        # This part and below is for CORS to work with my other domain
        # Handle OPTIONS requests
         if ($request_method = OPTIONS) {
            add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com;
            add_header 'Access-Control-Allow-Headers' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            return 204;
        }

        # CORS headers for other requests
        add_header 'Access-Control-Allow-Origin' 'https://headscale-admin.mydomain.com';
        add_header 'Access-Control-Allow-Headers' '*';
}

@gregoryca
Copy link

For @mich2k and anyone else looking for the traefik config:

   - "traefik.http.middlewares.corsHeader.headers.accesscontrolallowmethods=GET,OPTIONS,PUT"
   - "traefik.http.middlewares.corsHeader.headers.accesscontrolallowheaders=Authorization, *"
   - "traefik.http.middlewares.corsHeader.headers.accesscontrolalloworiginlist=https://headscale-ui.domain,https://headscale-ui.domain/"
   - "traefik.http.middlewares.corsHeader.headers.accesscontrolmaxage=100"
   - "traefik.http.middlewares.corsHeader.headers.addvaryheader=true"

This should work, and prevent the error @B08Z encountered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working stale
Projects
None yet
Development

No branches or pull requests