gotify / server

A simple server for sending and receiving messages in real-time per WebSocket. (Includes a sleek web-ui)
https://gotify.net
Other
10.43k stars 593 forks source link

403 when passing Origin header with Environment config #633

Open Ataye opened 4 months ago

Ataye commented 4 months ago

Have you read the documentation?

You are setting up gotify in

Describe your problem I have Gotify 2.4.0 running in Docker (using Caprover), with nginx proxy. Web UI runs fine (same origin). Using Postman from Windows also submits messages just fine. The problem is the cors allow origin seems to not be validating the Origin header and logging 403.

I have the following environment variables set (square brackets represent the UI textboxes of Caprover):

[GOTIFY_SERVER_CORS_ALLOWORIGINS] [- \".*\"]
[GOTIFY_SERVER_RESPONSEHEADERS] [X-Custom-Header: test only]

The Server is running from hosting.my-server.com, and Brave is running from localhost:3000'. Brave passes Origin ashttp://localhost:3000/` as it should as its cross-origin. A postman request with NO Origin results in 200 status, but if i add the same Origin header it fails with 403.

I have tried a bunch of different origin header values but none of them seem to work. Im guessing nginx proxy is not sending Origin but rather X-Forwarded-For which Gotify isn't reading. Or my regex is wrong. Surely .* will match anything?

Adding 'Access-Control-Allow-Origin' '*' to nginx only replies to the client with accepted, Gotify still logs 403.

What can i do here?

Any errors, logs, or other information that might help us identify your problem

Gotify logs for successful and failed requests:

2024-02-07T00:29:49.945130827Z 2024-02-07T00:29:49Z | 200 | 2.456346ms | 1.146.13.223 | POST "/message"
2024-02-07T00:29:50.127354894Z 2024-02-07T00:29:50Z | 200 | 230.165µs | 1.146.13.223 | GET "/static/defaultapp.png"
2024-02-07T00:29:56.480660944Z 2024-02-07T00:29:56Z | 403 | 36.116µs | 1.146.13.223 | POST "/message"

Proxy settings for nginx:

                proxy_pass $upstream;
                proxy_set_header Host $host;
                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;
jmattheis commented 4 months ago

Hey could you try configuring it like this?

version: "3"

services:
  server:
    image: gotify/server:2.4.0
    ports:
      - 8080:80
    environment:
      - GOTIFY_SERVER_CORS_ALLOWORIGINS=[.*]
    volumes:
      - "./gotify_data:/app/data"

This works for me when accessing cross domain: image

Ataye commented 4 months ago

Unfortunately i don't have access to the config file (actually, there is no config file, its all managed via env variables). It's running via Caprover which uses Docker under the hood. No access to the Dockerfile. It passes any environment vars through though, and i've checked this with printenv after an interactive shell:

GOTIFY_SERVER_RESPONSEHEADERS=X-Custom-Header: test only
HOME=/root
GOTIFY_SERVER_STREAM_ALLOWEDORIGINS=- \"*\"\n- \"localhost\"
TERM=xterm
SHLVL=1
GOTIFY_SERVER_PORT=80
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
GOTIFY_SERVER_CORS_ALLOWORIGINS=- \".+.example.com\"\n- \"otherdomain.com\"
_=/usr/bin/printenv
OLDPWD=/app

And with the new env:

GOTIFY_SERVER_CORS_ALLOWORIGINS=[.*]

Caprover does map a volume however, so i did create a simple config there but still no joy. This is the config:

server:
  responseheaders:
    X-Custom-Header: "has a config!"
cors:
  alloworigins:
    - [.*]
    - ".*"
  allowmethods:
    - "OPTIONS"
    - "GET"
    - "POST"

Interstingly, your screenshot doesn't show the preflight OPTIONS call so i guess it assumes its a simple call and skips it. I've switched to vite (from next.js) and axios (rather than fetch) and i am getting the same results. The response from the OPTIONS call is 204 (which is ok), and returns these response headers:

Access-Control-Allow-Methods: \"GET\"\N- \"POST\"\N- \"OPTIONS\"
Access-Control-Allow-Origin: http://localhost:5174
Access-Control-Max-Age: 43200
Connection: keep-alive
Content-Type: application/json
Date: Thu, 08 Feb 2024 22:40:13 GMT
Server: nginx
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Custom-Header: has a config!

Why is Allowed-Methods still in the env format (as per the docs), and not parsed out to GET, POST, OPTIONS? Anyway, it returns the correct Allow-Origin so the resulting POST should work:

image

(I removed the X-Gotify-Key header from the call and appended to the url) Log is showing the 204:

2024-02-08T22:40:13.737045928Z 2024-02-08T22:40:13Z | 204 | 37.285µs | 1.146.13.223 | OPTIONS "/message?token=[masked]"
2024-02-08T22:47:07.871513655Z 2024-02-08T22:47:07Z | 204 | 47.468µs | 1.146.13.223 | OPTIONS "/message?token=[masked]"
2024-02-08T22:54:53.848293265Z 2024-02-08T22:54:53Z | 204 | 80.573µs | 1.146.13.223 | OPTIONS "/message?token=[masked]"

Postman continues to work correctly with and without the Origin header being sent, but it doesn't care about cors.

jmattheis commented 4 months ago

Don't mix environment config and file config. I've updated the environment config in the docs https://gotify.net/docs/config could you retry it in this format?

I've this docker-compose:

version: "3"

services:
  server:
    image: gotify/server:2.4.0
    ports:
      - 8080:80
    environment:
      - GOTIFY_SERVER_CORS_ALLOWORIGINS=[dyne\.local:3000]
      - GOTIFY_SERVER_CORS_ALLOWMETHODS=[GET, POST]
      - GOTIFY_SERVER_CORS_ALLOWHEADERS=[x-gotify-key]
      - 'GOTIFY_SERVER_RESPONSEHEADERS={X-Custom-Header: "custom value", x-other: value}'
    volumes:
      - "./gotify_data:/app/data"

with this html script:

<script>
  fetch("http://localhost:8080/message?message=x", {
    method: "POST",
    headers: { "x-gotify-key": "AgarXhfGWhnzOae" },
  });
</script>

This works fine for me, now with the options/preflight request:

Responses

## Normal Response ![image](https://github.com/gotify/server/assets/14895212/4c9bf0f0-b71b-4c37-8151-196741ca4f06) ## OPTIONS Response ![image](https://github.com/gotify/server/assets/14895212/66b1d88f-ffc2-4181-8275-898108260c94)

If you set the environment variables similar to this and it still doesn't work, then caprover does something weird with environment variables.

Ataye commented 4 months ago

Ok. First up, yes, that fixed the Origin issue with the env vars!! So i removed the temp config i made, and set the env vars:

image

And now i can see the correct Allow-Methods being returned, yay!

However, your code with fetch does not work :(

image

But this does:

    fetch(`${HOST}/message?token=${APP_KEY}&message=x2`, {
      method: "POST",
    });

And so does this:

    fetch(`${HOST}/message?token=${APP_KEY}`, {
      method: "POST",
      headers: { 
        "Content-Type": "application/x-www-form-urlencoded"
      },
      body: "message=urlencoded from react with fetch"
    });

And this:

axios.post(
      `${HOST}/message?token=${APP_KEY}`, 
      {
        message: 'urlencoded from react with axios'
      },
      {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
    })

If i put the token in the headers it fails, which indicates its something to do with the proxy. I've explicitly tried setting proxy_pass_request_headers on; but doesn't make a difference. It's also not complaining about needing to provide an access token when so seems to be sending the token through though!

What's more, if i send json it sets the content-type header accordingly and whether i have token in headers or in the QS it causes a preflight cors OPTIONS check and returns 204 and fails cors.

So summary: Setting token in Header does not work. Posting as json does not work. Posting with X-Gotify header works in Postman, as does json format!!!

Responses Options ![image](https://github.com/gotify/server/assets/3684664/47512dea-6050-45e6-ad94-d4c13f2be97d) Normal ![image](https://github.com/gotify/server/assets/3684664/f5bc7618-2f6a-4628-bdb9-dbc28394e7f6) Payload ![image](https://github.com/gotify/server/assets/3684664/4c70120b-d5c2-4b5a-a450-ba993fc2e304)

And here are the various functions i've used:

Fetch requests working / not working ```js /// WORKS const doFetch1 = async () => { axios.post( `${HOST}/message?token=${APP_KEY}`, { message: 'urlencoded from react with axios' }, { headers: { 'Content-Type': 'multipart/form-data' }, }) } /// DOESN't WORK const doFetch2 = async () => { fetch(`${HOST}/message?message=x1`, { method: "POST", headers: { "x-gotify-key": APP_KEY }, }); } /// WORKS const doFetch3 = async () => { fetch(`${HOST}/message?token=${APP_KEY}&message=x2`, { method: "POST", }); } /// WORKS const doFetch4 = async () => { fetch(`${HOST}/message?token=${APP_KEY}`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "message=urlencoded from react with fetch" }); } /// DOESN'T WORK const doFetch5 = async () => { fetch(`${HOST}/message?token=${APP_KEY}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: { message:"json from react with fetch" } }); } /// DOESN'T WORK const doFetch6 = async () => { axios.post( `${HOST}/message?token=${APP_KEY}`, { message: 'json content type message' }) } ```

It's interesting json doesn't work as the library i've used "gotify-client": "^0.4.2", createMessage uses json as its message format, which means that library doesn't work for me (plus it sets the X-Gotify-Key header, also a no-go).

https://github.com/hywax/gotify-client/blob/fedddd34f119cfa748f7fac19f640a2e8a4a085e/src/api/MessageApi.ts#L30

And i haven't even gotten to testing websocket yet, now that message create is working for me :/

jmattheis commented 4 months ago

Have a look at the browser console, it says why the CORS request fails. Headers like Content-Type and X-Gotify-Key must be whitelisted in cors. You can do this with GOTIFY_SERVER_CORS_ALLOWHEADERS=[X-Gotify-Key, Content-Type]

With this config requests with json and token inside the header work fine.

  fetch("http://localhost:8080/message", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Gotify-Key": "AgarXhfGWhnzOae",
    },
    body: JSON.stringify({ message: "json from react with fetch" }),
  });
Ataye commented 4 months ago

🤦‍♂️Ahhh, i can't believe i missed that, and i was just literally looking at the allowed headers option in the docs!!! Yesss, thank you!

Well that about sorts it. Difference between Axios and Fetch is Axios automatically stringifies the data object, fetch doesn't, which is why i just got the bad requests in the console.

Oh, i just tested standard websocket to receive messages and its happy now as well with useWebSocket passing token in the url. I tried gotify-client client.stream.streamMessages() but so far haven't been successful, with errors like Bad Request etc. Mainly because of lack of docs on how to use, so i'll dig in and figure it out.

So, the take from this i think is that there either needs to be a section in the docs like

For Cross Site calls, this is the minimum required set of config options (replace .* with the regex for your allowed origin, and x-gotify-key to allow the token in the header, and Content-Type to allow application/json etc):

GOTIFY_SERVER_CORS_ALLOWORIGINS=[.*]
GOTIFY_SERVER_STREAM_ALLOWEDORIGINS=[.*]
GOTIFY_SERVER_CORS_ALLOWMETHODS=[GET, POST]
GOTIFY_SERVER_CORS_ALLOWHEADERS=[X-Gotify-Key, Content-Type]

Or those Methods and Headers are allowed by default? (I understand the implications of allowing every Origin, so totally fine to have the user supplied something sensible).

However, i do appreciate you updating the docs with the correct format for the env vars, cheers :)

jmattheis commented 4 months ago

I'd say adding the allowheaders and allowmethods as defaults shoulds reasonable.