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

Path probing fails in presence of redirects #9134

Open
drigz opened this issue Jun 10, 2024 · 14 comments
Open

Path probing fails in presence of redirects #9134

drigz opened this issue Jun 10, 2024 · 14 comments
Labels
kind/bug Categorizes issue or PR as related to a bug.

Comments

@drigz
Copy link

drigz commented Jun 10, 2024

What happened?

We're hosting the dashboard behind an auth proxy on a subpath. However, we find that unless we create special ingress rules to work around this, the dashboard doesn't work in this environment:

  • Open Chrome inspector.
  • Navigate to https://example.com/kubernetes-dashboard/
  • Observe a request to /assets/config/config.json (from this probing code)
  • The application assumes this is served by the single-page app and serves a redirect to the login page.
  • It then serves status code 200 and HTML on the login page.

The tab remains blank.

What did you expect to happen?

Probing requests that return 3xx status codes or HTML content are ignored, and the probing continues until it reaches the "real" config.json.

How can we reproduce it (as minimally and precisely as possible)?

I don't have a repro as it'd take some work to put one together with kind or similar rather than with our cloud infra - let me know if that would be decisive for whether this is addressed and/or whether a PR is accepted.

Anything else we need to know?

I was able to work around this by creating a new Ingress resource to serve errors on /assets/config/config.json for the probed, incorrect paths. Creating a new subdomain to expose the dashboard would have also worked.

What browsers are you seeing the problem on?

No response

Kubernetes Dashboard version

7.4.0

Kubernetes version

1.29.4

Dev environment

No response

@drigz drigz added the kind/bug Categorizes issue or PR as related to a bug. label Jun 10, 2024
@floreks
Copy link
Member

floreks commented Jun 11, 2024

We can extend the logic that tries to dynamically configure base href. It's quite annoying to configure an environment locally that will allow testing more complex scenarios like yours. If you've got an environment where you could test those changes, feel free to open a PR.

@Palollo
Copy link

Palollo commented Jul 16, 2024

I think I am in the same case...
The problem is here:

      const probePath = 'assets/config/config.json';
      const origin = document.location.origin;
      const pathSegments = document.location.pathname.split('/');

      let basePath = '/';
      let configFound = false;

      for (let i = 0; i < pathSegments.length; i++) {
        const segment = pathSegments[i];
        if (segment.length > 0) {
          basePath = basePath + segment + '/';
        }

        const fullPath = origin + basePath + probePath;
        configFound = configExists(fullPath);
        if (configFound) {
          break;
        }
      }

As @drigz, I have the kubernetes dashboard in the path https://example.com/kubernetes-dashboard/, so the value of pathSegments is ["","kubernetes-dashboard",""].
The "for" loop is looking for config files in all the base paths.
When it tries with the first one "",
basePath = "/" + "" + "assets/config/config.json",
then configExist("https://example.com/assets/config/config.json") returns true because I have another application in that path which has another different "config.json" file!
And the browser network log says it is corrupted (NS_ERROR_CORRUPTED_CONTENT), because it is not the expected config file.

Why it is looking for a config file in base paths??
It should look for the config file in the complete path: **https://example.com/kubernetes-dashboard/**assets/config/config.json
and not in the previous paths like this one: **https://example.com/**assets/config/config.json

It works when I change the previous code to that (taking the configExist() out of the loop, to try only the final complete path):

      const probePath = 'assets/config/config.json';
      const origin = document.location.origin;
      const pathSegments = document.location.pathname.split('/');

      let basePath = '/';
      let configFound = false;

      for (let i = 0; i < pathSegments.length; i++) {
        const segment = pathSegments[i];
        if (segment.length > 0) {
          basePath = basePath + segment + '/';
        }
      }
      const fullPath = origin + basePath + probePath;
      configFound = configExists(fullPath);

@floreks
Copy link
Member

floreks commented Jul 18, 2024

The reason it starts from the root and goes up is that if you i.e. use a direct link to a subview in Dashboard, it has to be able to find a base path it can use in combination with file subpath to get the config. It also doesn't know if it is being served on a root path or on a sub path thus starting from the root.

Consider these examples:

https://example.com - access main view
https://example.com/pods/xyz - access some pod details view

https://example.com/dashboard - access main view on a subpath
https://example.com/dashboard/pods/xyz - access some pod details view on a subpath

It could start from the full path and subtract parts of it, but unfortunately, it will be suboptimal. Long URLs will take longer to load initially as it will have to traverse through more paths to find the correct one.

@Palollo
Copy link

Palollo commented Jul 21, 2024

Thanks for your quick response @floreks,
I don't know if I have different version than yours or different configuration, but in my case, the direct links are like this:

https://example.com/dashboard/ - access main view on a subpath - pathSegments is ["","dashboard",""]
https://example.com/dashboard/#/pod/mynamespace/xyz - access some pod details view on a subpath - pathSegments is the same ["","dashboard",""]

The path within the kubernetes-dashboard app is specified after the hash char, i.e. in the fragment part, not in the path part.
So I think the solution you propose is good!
Even if the URLs were like you say, the looking for the config file would be only in the initial access. And not so long, probably 2-3 levels, not more, I think. In my case 0 levels ;)

@Palollo
Copy link

Palollo commented Jul 21, 2024

It is better to have a solution working for all the cases, although suboptimal in some specific cases,
than a solution optimal for some cases and not working on some other cases.

@drigz
Copy link
Author

drigz commented Jul 22, 2024

I have another application in that path which has another different "config.json" file!

Does your application serve valid JSON from /assets/config/config.json? What does the JSON look like, compared to the config from the application itself?

I think the "root cause" here is the false positive, and while reversing the search order trades speed against false-positive risk, another way to address it would be to make the check less likely to trigger falsely. I'd previously thought it would be enough to check for valid JSON that looks like dashboard config, but I'm now questioning whether there should be a purpose-specific /assets/sentinel.json that is unmistakeable in its content. (that said, I haven't had/taken the time to set up a dev environment and try this, so feel free to ignore my ideas in favor of those with time to work on this)

@Palollo
Copy link

Palollo commented Sep 2, 2024

Yes, that would be another solution in case of the search is required: start from the root path but check for a unique field value in the config file.
As I said, in our case the search is not required and it's perfectly working.

This is the code we currently have:

      let url = document.location
      const configUrl = url + 'assets/config/config.json';
      configFound = configExists(configUrl);
      
      document.write("<base href='" + (configFound ? url : '') + "' />");

Instead of this:

      const probePath = 'assets/config/config.json';
      const origin = document.location.origin;
      const pathSegments = document.location.pathname.split('/');

      let basePath = '/';
      let configFound = false;

      for (let i = 0; i < pathSegments.length; i++) {
        const segment = pathSegments[i];
        if (segment.length > 0) {
          basePath = basePath + segment + '/';
        }

        const fullPath = origin + basePath + probePath;
        configFound = configExists(fullPath);
        if (configFound) {
          break;
        }
      }

      document.write("<base href='" + (configFound ? basePath : '') + "' />");

@fmichea
Copy link

fmichea commented Oct 1, 2024

@drigz When you originally wrote this issue, do you mind me asking to clarify if the tab did remain blank and prevent login or if you get something on the page ? Has this been fixed ? Is the behavior still the same today ?

The What did you expect to happen? section of your original comment and the following conversation makes it difficult to understand if you had false positives like @Palollo is describing or if you ran into a blank page even without those false positives. Did the correct sub-path get found in your case ?

On my end, I am seeing the same probing code go through the same steps, it does get a 404 for the first level, then finds the file under the correct path, so I think the code should be working as intended and finding the correct base path, but I still see a blank login page... for some time. Currently digging into that but want to confirm the behavior you observed.

In any case, I think it's fairly commons for web apps to allow specifying a sub-directory it is served under as an option. That would remove the use of probing code entirely and seems pretty straightforward. What do you think?


Here is how my case continues for the curious, I dont think it's 100% related :

  • Page is blank but seems to query /api/v1/me repeatedly, on the server side (nginx-ingress, kong) I see them responding with 499 codes which appears to be an nginx specific code. On firefox's side it looks like the requests are just haging until they display as NS_BINDING_ABORTED.
  • After some time (minutes), a /settings request is made and the login form is finally shown.
  • Submitting anything to the form causes requests to /api/v1/csrftoken/login which end in a similar result as the /api/v1/me requests except they show as NS_ERROR_NET_RESET. Looking into that currently as that could be an nginx ingress misconfiguration.

@floreks
Copy link
Member

floreks commented Oct 2, 2024

Your case is different and looks to be an ingress misconfiguration. Did you disable kong and try to replace it with ingress?

@drigz
Copy link
Author

drigz commented Oct 2, 2024

When you originally wrote this issue, do you mind me asking to clarify if the tab did remain blank and prevent login or if you get something on the page ? Has this been fixed ? Is the behavior still the same today ?

The tab remained blank after a false positive (a 200 response with HTML in the response body) so I think it's the same thing Palollo described.

I haven't tried updating or removing my workaround since I filed this - I didn't see anything indicating that the probing behavior changed, and I didn't take the time to try to adjust it myself.

In any case, I think it's fairly commons for web apps to allow specifying a sub-directory it is served under as an option. That would remove the use of probing code entirely and seems pretty straightforward. What do you think?

I actually quite like the probing as we sometimes access the backend from a different path (when port-forwarding past our ingress directly to the dashboard backend). But I'd be OK with the fixed path.

@fmichea
Copy link

fmichea commented Oct 2, 2024

Cool thanks for confirming, as expected my issue was unrelated but I think this is good context. It's possible that having the option for a fixed prefix defined by configuration could be in addition to the probing code. Then the configured path would take precedence to probing and allow anyone who wants to bypass that piece of code without introducing a workaround. Just a thought, leaving you to decide on the next steps regarding this issue :) Thanks again

@floreks
Copy link
Member

floreks commented Oct 3, 2024

Unless something has changed in angular in the latest version that I am not aware of, there is no way to dynamically adjust app path during runtime via config. Path is part of the main HTML file and base url has to be adjusted there to work with the sub path. It can be only changed during the build or via JavaScript.

@Palollo
Copy link

Palollo commented Oct 4, 2024

Ok, my last attempt, if you want to fix now this issue, the conservative change I propose...
Just check if config file exists (in the current location), and if not then do the search as you did, the optimal way.
The "else" content is exactly the same previous code.

      let url = document.location
      const configUrl = url + 'assets/config/config.json';
      if (configExists(configUrl)) {  
        document.write("<base href='" + url + "' />");      
      }
      else {    // search for the config
        const probePath = 'assets/config/config.json';
        const origin = document.location.origin;
        const pathSegments = document.location.pathname.split('/');

        let basePath = '/';
        let configFound = false;

        for (let i = 0; i < pathSegments.length; i++) {
          const segment = pathSegments[i];
          if (segment.length > 0) {
            basePath = basePath + segment + '/';
          }

          const fullPath = origin + basePath + probePath;
          configFound = configExists(fullPath);
          if (configFound) {
            break;
          }
        }

        document.write("<base href='" + (configFound ? basePath : '') + "' />");
      }

Instead of this:

      const probePath = 'assets/config/config.json';
      const origin = document.location.origin;
      const pathSegments = document.location.pathname.split('/');

      let basePath = '/';
      let configFound = false;

      for (let i = 0; i < pathSegments.length; i++) {
        const segment = pathSegments[i];
        if (segment.length > 0) {
          basePath = basePath + segment + '/';
        }

        const fullPath = origin + basePath + probePath;
        configFound = configExists(fullPath);
        if (configFound) {
          break;
        }
      }

      document.write("<base href='" + (configFound ? basePath : '') + "' />");

@BigFlagBurito
Copy link

Hi, I have a different problem, but the same loop seems to be the cause of the problem.

Briefly summarised my configuration:

  • I am using the Ingress NGINX controller
  • There are two ingresses configured. One for / and one for /dashboard

Due to the development of my web app, which can be accessed via /, an error occurred there. A request blocked the entire web app. This meant that other requests could no longer get through. When I wanted to find out why I could no longer access the website, I wanted to run a diagnosis via the dashboard. But for some inexplicable reason, I couldn't access the dashboard until the request to the web app timed out.
After a very, very long search, I found the problem why I couldn't reach the dashboard.
As it turned out, the dashboard makes a request to / for the assets.

As @fmichea suggested, it would be great if there could be an option to specify which sub-path the dashboard is accessible on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/bug Categorizes issue or PR as related to a bug.
Projects
None yet
Development

No branches or pull requests

5 participants