mainsail-crew / mainsail

Mainsail is the popular web interface for managing and controlling 3D printers with Klipper.
https://docs.mainsail.xyz
GNU General Public License v3.0
1.67k stars 346 forks source link

Add nginx reverse proxy support #1163

Open EricZimmerman opened 1 year ago

EricZimmerman commented 1 year ago

Requested feature:

I tried for hours to get this working with nginx to no avail. Most apps seem to want a urlprefix for things to work and this was my issue I believe since the app always looked at the root of the url for various files.

I tried rewrite rules and a bunch of other things. Other examples are sonarr and radar, among others

Solves the following problem:

Allow for security to be managed in nginx including SSL, authentication and 100 other things.

Additional information:

No response

meteyou commented 1 year ago

Allow for security to be managed in nginx including SSL, authentication and 100 other things.

All of that is possible with the current state.

EricZimmerman commented 1 year ago

so, is this something where you would just replicate the existing docs on nginx on another nginx instance? i was trying to essentially proxy the nginx instance on the pi to my external nginx box.

EricZimmerman commented 1 year ago

i dont think it is, or its not obvious how to do this.

even trying to use the nginx docs on a different instance of nginx (not on the same site) its failing to find the files:

image

lets say i have http://voron24/ which is mainsail and working just fine.

i have another instance of nginx that is my proxy, running on 192.168.1.xxx. this is exposed to the internet via port forwarding, has auth in front of it, ssl, etc.

i want to set up http://www.publicsite.com/voron24 and have it proxy to http://voron24/ but i have never been able to get this working, as it would always attempt to at least load the websocket from the root of the app vs the actual url.

this is where the whole urlbase thing comes into play, ala Radarr, so that all URLs essentially get rewritten with this base.

i think this is where things are breaking, at least in the scenario above.

unless i am missing something, i dont see how "this already works" when mainsail is on the pi and nginx is serving files from there, so any guidance, or an acutal working reverse proxy block, would be awesome.

working example for radarr

location /radarr {

    auth_basic "Not for you area";
    auth_basic_user_file /etc/nginx/.htpasswd;

    proxy_pass http://192.168.1.XXX:7878;
    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_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $http_connection;

}

what should be possible is to replicate this, but change the proxy_pass value to http://voron24/ (in my case), but it does not. things start loading, and then the websocket, trying to be loaded/contacted from /, fails, which then kills things Image i tried playing with the examples at https://docs.mainsail.xyz/setup/manual-setup/mainsail to see if i could get things working on my external proxy via something like:

location /voron24/websocket { proxy_pass http://apiserver/websocket; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 86400; }

but this did not work.

i even tried goofy stuff such as:

sub_filter 'src="/assets/index.37dba9ad.js'  'src="/voron/assets/index.37dba9ad.js';
sub_filter 'href="/assets/index.65709a8d.css'  'href="/voron/assets/index.65709a8d.css';

but of course, the file names change, so that wont work.

please prove me wrong here =)

if this is possible in the current state of things, please provide a working example like the one above for /radarr

vajonam commented 1 year ago

having a base url setting simplifies having sub folder based reverse proxies. server name voron24.domain.com works OOB because it uses the / context for stuff.

EricZimmerman commented 1 year ago

Except for webcam access, or at least it didn't when I did the subdomain approach.

One, or both, options should work ideally

On Fri, Nov 25, 2022, 8:14 AM vajonam @.***> wrote:

having a base url setting simplifies having sub folder based reverse proxies. server name voron24.domain.com works OOB because it uses the / context for stuff.

— Reply to this email directly, view it on GitHub https://github.com/mainsail-crew/mainsail/issues/1163#issuecomment-1327466095, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABARKJXXQ26JR25UHEARDCLWKC3TZANCNFSM6AAAAAASGSWOVE . You are receiving this because you authored the thread.Message ID: @.***>

SoloTSi97 commented 1 year ago

@EricZimmerman Obviously, this issue is still open but I wonder if you made any further progress on your configuration? This is exactly my setup and I'm having similar struggles. Thanks!

EricZimmerman commented 1 year ago

Nope. I gave up. Not worth the hassle without basic support for this imo.

I just use zerotier

Roguyt commented 1 year ago

I currently have a working solution, which the only caviat is regarding the basic authentification. Sometimes it doesn't renew so I need to open like the webcam stream URL to prompt it again and I'm good to go.

image

EricZimmerman commented 1 year ago

Can you post it?

Roguyt commented 1 year ago

Sure thing.

All its missing is Mainsail having native authentication to remove the basic nginx authentication.

server {
        listen 80;
        server_name mainsail.baguette.fr;

        location ~ /\.well-known/acme-challenge {
                root /var/www/html;
                allow all;
        }

        location / {
                return 301 https://$server_name$request_uri;
        }
}

server {
        listen 443 ssl;
        server_name mainsail.baguette.fr;

        add_header Strict-Transport-Security "max-age=31536000";
        ssl_certificate /etc/letsencrypt/live/mainsail.baguette.fr/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/mainsail.baguette.fr/privkey.pem;
        ssl_trusted_certificate /etc/letsencrypt/live/mainsail.baguette.fr/chain.pem;

        access_log /var/log/nginx/mainsail.baguette.fr.access.log;
        error_log /var/log/nginx/mainsail.baguette.fr.error.log error;

        client_max_body_size 100m;

        auth_basic "";
        auth_basic_user_file /etc/nginx/.htpasswd;

        location /nginx_status {
                stub_status on;
                access_log off;
        }

        location / {
                proxy_set_header Host $http_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;
                proxy_pass http://votre_ip:80;
        }

        location /websocket {
                proxy_pass http://votre_ip:80/websocket;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "Upgrade";
                proxy_set_header Host $http_host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_read_timeout 86400;
        }
}
jbyerline commented 1 year ago

Has anyone had any luck getting the webcam to be viewable through the proxy? I get CORS error. I have everything else working though.

meteyou commented 1 year ago

@jbyerline depends on which cors errors you get. But I think you get cors errors from the API (Moonraker). You have to add your domain/url to cors_domains in your moonraker.conf.

Roguyt commented 1 year ago

Has anyone had any luck getting the webcam to be viewable through the proxy? I get CORS error. I have everything else working though.

The configuration I posted earlier should work. It shouldn't work if you are using crownest v4 and webrtc streaming, for that I am waiting for the update of the library used by crownest to allow remote ICE.

dictor93 commented 11 months ago

Has anyone had any luck getting the webcam to be viewable through the proxy? I get CORS error. I have everything else working though.

The configuration I posted earlier should work. It shouldn't work if you are using crownest v4 and webrtc streaming, for that I am waiting for the update of the library used by crownest to allow remote ICE.

This configuration works well, but when i use port forwarding from the nginx proxy by the router and if i don't want to use the external 443 port (another service already occupied it) camera streaming is not possible, because the camera url is composed despite current used port. We can add some checking and port here some kind of that:

getHostUrl: (state) => {
        const port = window.location.port
        return (state.protocol === 'wss' ? 'https' : 'http') + '://' + state.hostname + (port.length ? `:${port}` : '' ) + '/'
},
meteyou commented 11 months ago

@dictor93 this is a browser limitation. If you want to use a Https Webinterface, all other connections have also to be secure! You cannot connect to an insecure webcam with a secure Webinterface.

dictor93 commented 11 months ago

@dictor93 this is a browser limitation. If you want to use a Https Webinterface, all other connections have also to be secure! You cannot connect to an insecure webcam with a secure Webinterface.

What nonsense are you talking about? Data from the camera is received through the endpoint of the same server on which Mainsail works and to which a reverse proxy is made. It's just that if I'm using a different port than 443, I want the path to the camera endpoint to include that port as well and not refer to 80/443.

I compiled the frontend with the changes I mentioned above and have no problems with the camera. But for this you will have to give up updates. Or build the frontend with your own edits every time. I just suggested making it part of the codebase

PSandro commented 7 months ago

Hi, I'd like to work on this issue (or similar). As I see it, the problem is not adding "reverse proxy support" specifically for nginx but rather enable mainsail to configure a BASE_URL. This will be especially useful when setting up multiple printers on the same domain name behind a reverse proxy e.g.

http://example.com/printer1/ http://exmaple.com/printer2/ http://example.com/printer3/

and so on.

EricZimmerman commented 7 months ago

I brought this up as well and was told it's way too much work but that was 2 years ago

t3chguy commented 7 months ago

Not quite what is being asked for subpath reverse proxying, but I accomplished multiple cameras on a single domain.

I ended up using a single Mainsail instance for multiple printers using the following Caddy config:

    @3dp host 3dp.bit.ovh
    handle @3dp {
        import authentik

        handle_path /printers/e3v2/* {
            reverse_proxy http://10.10.100.104:7125
        }
        handle_path /printers/v0-417/* {
            reverse_proxy http://10.10.100.103:7125
        }
        handle /webcam/* {
            @webcam_e3v2 query printer=e3v2
            reverse_proxy @webcam_e3v2 http://10.10.100.104:8080
            @webcam_v0-417 query printer=v0-417
            reverse_proxy @webcam_v0-417 http://10.10.100.103:8080
        }
        handle {
            #            reverse_proxy http://fluidd:80
            reverse_proxy http://mainsail:80
        }
    }

along with this Mainsail config

{
    "instancesDB": "json",
    "instances": [
        { "hostname": "3dp.bit.ovh", "port": "443/printers/v0-417/" },
        { "hostname": "3dp.bit.ovh", "port": "443/printers/e3v2/" }
    ]
}

And the cameras are included as /webcam/?action=stream&printer=e3v2 etc

The camera trick allows a single camera entry to work on both the shared domain and on the printer directly for things like the Moonraker-homeassistant integration

meteyou commented 7 months ago

@PSandro hey! I appreciate your interest in an implementation.

If you mean the BASE_URL of vite, you must be careful that this can only be used for a new build but not in live. A possibility via the config.json would be excellent, but I don't know how well you can do that because it has to be recognized on which level it is located (if you open mainsail not at the root level, but directly a subsite).

There was already a PR on this topic, but unfortunately, it was not completed: https://github.com/mainsail-crew/mainsail/pull/1359

if you have any questions, please ping me or contact me on discord.

PhilippMolitor commented 1 month ago

There should be a possibility to dynamically detect the actual base path via the experimental vite base options: https://vitejs.dev/guide/build.html#advanced-base-options Either it has do do some kind of detection logic to test what part of the base path in the window context results in valid requests, or you'd have to rely on some kind of injection or templating in nginx/caddy to replace a global base path variable in index.html.

meteyou commented 1 month ago

@PhilippMolitor pay attension, that vite is only a "build framework". so it only runs during the build/release process.

if i read this description:

A single static base isn't enough in these scenarios. Vite provides experimental support for advanced base options during build, using experimental.renderBuiltUrl.

it sounds also, that it only build the url during the build process and not "live".

PhilippMolitor commented 1 month ago

I know, I am using vite a lot myself. What I am saying is that you could inject your own function there, wrapping the existing one, reading a global variable defined (for example) in index.html. This way you could use the build process to make it handle base path detection at runtime depending on some variable.

meteyou commented 1 month ago

We try to store all non "Moonraker instance related variables" in the config.json file and there i have the first problem, because the path/url cannot be found without the base_url. this file also has the background, because you can exclude it from the Moonraker update_manager.

i have now read the guide again carefully, but unfortunately i still can't recognize any “dynamic update” after the build process. to me it looks like a hardcoded url made after the build.