JBildstein/SpiderEye

Set user agent

bddckr opened this issue · 9 comments

Describe the feature you'd like
I'd like to be able to set the user agent.

Reason for this feature request
By setting the user agent I can allow my backend server implementation to identify that the login session is from my app.

I just check the various webview APIs and it's probably possible for all of them.

WebBrowser/WebViewControl: UrlMkSetSessionOption in urlmon.dll
webkit2gtk: webkit_settings_set_user_agent
WKWebView: customUserAgent property

I couldn't find anything for webview2 (Edge Chromium) but that thing doesn't seem to be very complete anyhow.

Depending on what you are doing with that information this seems rather insecure though. Anyone could set that header in a different browser.

I just check the various webview APIs and it's probably possible for all of them.

Thanks for checking! That sounds good.

Depending on what you are doing with that information this seems rather insecure though. Anyone could set that header in a different browser.

That's fine - we're just interested in identifying the logged-in sessions in a UI that allows logging them out :)

I found something for the user agent and Edge Chromium now: MicrosoftEdge/WebView2Feedback#122 (perhaps you'd also like to give a thumbs up there)
It's currently not implemented and can only be set during initialization which would mean the user agent would have to be set in the Application class before any UI is created.
This might actually be a good idea anyway because from what I have read the other Windows webviews require the user agent to be set before creating the webview as well (though I haven't tried it myself yet).
Of course this would mean that the user agent will be the same for the whole application and cannot be changed for a specific window.

Thoughts?

Depending on what you are doing with that information this seems rather insecure though. Anyone could set that header in a different browser.

That's fine - we're just interested in identifying the logged-in sessions in a UI that allows logging them out :)

Ok good, I just saw "login session" and warning bells went off 😄

perhaps you'd also like to give a thumbs up there

Done! The proposed current solution is a bit involved, isn't it... We definitely want them to offer an easier way to set the user agent moving forward.

Thoughts?

I might be wrong here, but my gut feeling is that most users of webviews only present a single window to the user. As a replacement of Electron etc. you are ultimately presenting a SPA after all.
That matches my use case, so not being able to set the user agent per-window and instead just for the whole application works for me.
Additionally I think it makes sense for the user agent to be fixed after creation. Ignoring whether all platforms' webview components offer that as a feature to begin with, I'd argue that user agent has been traditionally a fixed thing for a reason. No one should depend too much on it really. It's supposed to explain the browser, not necessarily a particular window etc.


Ignoring what I just said about the need for this feature:

Perhaps for other fetching and querying setting the user agent can be done on each request actually? I just saw this issue, which made me look at the mentioned proposal, but also the currently available event. It looks like you can set the request's headers as part of this event. That then modifies the request object obviously, and as the event is fired before the request is sent off it should work. The other browser components for the other OSs might have similar functionality.
Perhaps this could allows us to have a per-window property that lists all headers - or even more advanced - a cross-platform callback API for all requests?

it's a bit involved, yes, but it's not a big change actually. I pushed the webview2 branch yesterday with an initial implementation of it, I'd just have to add the environment options to the init code:

private async Task InitWebview()
{
environment = await CoreWebView2Environment.CreateAsync();
await webview.EnsureCoreWebView2Async(environment);

thanks, that sounds quite reasonable. I think I'll implement something like Application.SetUserAgent(string userAgent).
A use case for different user agents per window might be if you have your own app and open a window to an external website. But since that's probably a rare case and technically a problem with the current APIs it'll just not be supported.


I think you slightly misunderstood that event but it would still be possible yes. That event doesn't actually send a request but allows you to intercept requests from the webview and then set your own response. The headers you can set are for the response. You can see it implemented for serving embedded files here:

private async void CoreWebView2_WebResourceRequested(object sender, CoreWebView2WebResourceRequestedEventArgs e)
{
var deferral = e.GetDeferral();
try
{
using (var contentStream = await Application.ContentProvider.GetStreamAsync(e.Request.RequestUri))
{
if (contentStream != null)
{
e.Response = environment.CreateWebResourceResponse(contentStream, 200, "OK", string.Empty);
}
else { e.Response = environment.CreateWebResourceResponse(null, 404, "Not Found", string.Empty); }
}
}
catch { e.Response = environment.CreateWebResourceResponse(null, 500, "Internal Server Error", string.Empty); }
finally { deferral.Complete(); }
}

(it doesn't currently work though due to a bug that is addressed in the issue you linked)

Still, you could simply send off your own HTTP request which is completely under your control and then use the response of that.
Essentially it would act as a proxy.
The other webviews have somewhat similar events by registering custom URI scheme handlers (I use spidereye:// for serving the embedded files) but I'm not sure if it's possible to handle http/s requests as well. It's certainly worth a look because I can see how that feature would be useful.

I have some bad news, the UrlMkSetSessionOption call doesn't seem to work with the Edge webview and I couldn't find any other API for it. So if I haven't missed anything, it's unlikely it'll ever get that feature (it'll be deprecated in favor of Edge Chromium pretty soon).

I don't think it makes sense to implement it if it doesn't work everywhere so I'll leave it at that for now. Once Edge Chromium is in GA and available on most machines it might make sense to revisit this.

I'll look into the proxy idea next and hopefully that'll help you, otherwise you'll have to look into alternatives like using ajax after the page load or communicating from the .NET side via Window.Bridge.

it'll be deprecated in favor of Edge Chromium pretty soon

Yeah well it will be nice to get some modern webtech available easier on Windows for sure. Too bad the deployment details are a bit more involved for me or my users.

Once Edge Chromium is in GA and available on most machines it might make sense to revisit this.

As seen in the linked issue: Microsoft is not expecting that "on most machines" to happen without our third party involvement (by installing the runtime for the user) at least. But technically at one point enough apps might have requested the runtime to be installed for it to already be there for our apps!

I don't think it makes sense to implement it if it doesn't work everywhere so I'll leave it at that for now.

That makes sense, and as far as I can see everything even down to the menu/tray icon support in SpiderEye is available on all three platforms, right? (Love that!)

[...] and I couldn't find any other API for it.

Does this help? It's more involved, but that should work I think. It basically goes down the "proxy" path, doesn't it. In the case that this per-request solution isn't possible for the other components SpiderEye could do still offer the original proposed API of yours - an initialization-time setting, which secretly is actually implemented in the linked workaround-ish fashion for Edge only.

Sorry if I'm getting confused now with all the webview components, but is this the current state?

  1. WebBrowser/WebViewControl should work with UrlMkSetSessionOption, but it looks like it doesn't do anything. My linked workaround that is more of a proxy solution might work here.

  2. WebView2 does not yet have proper API for this, but you can set it via the options. You said this isn't implemented yet, but they are saying it is AFAICS?

    For now, you can pass the --user-agent browser args to CreateWebView2EnvironmentWithDetails.
    MicrosoftEdge/WebView2Feedback#122 (comment)

    (A later comment says how to do it in C#.)

  3. webkit2gtk offers API.

  4. WKWebView offers API, too.

[...] otherwise you'll have to look into alternatives like using ajax after the page load or communicating from the .NET side via Window.Bridge.

I'm checking with the rest of the team whether we can do any of that, as this is a component not under our control...

I was under the impression that Edge Chromium would be shipped to everyone and with it webview2, guess I didn't read that careful enough...
I might be able to make things a bit easier by adding a small utility class that downloads the webview installer and executes it for you (with permission of the user obviously). It'd be like described in the linked issue and wouldn't incur any penalty in app size or require an installer to be shipped for the app. It would require an internet connection though.

That makes sense, and as far as I can see everything even down to the menu/tray icon support in SpiderEye is available on all three platforms, right? (Love that!)

Yes, all features are supported on all platforms unless the concept doesn't exist (I think the only thing where that applies is the window icon on macOS which is simply ignored). Some things that only exist on one platform are implemented in the platform specific assemblies, like the app menu on macOS.

Your linked SO answer is similar to the proxy solution yes, it does however only work on the page request and not on any subsequent resource requests (js, css, images etc.). I think the proxy solution would be the more thorough and flexible solution (if I can get it to work). I'll try to include methods that make it as easy to use as possible so you just have to change/add headers and the rest of the request is handled for you.

The state of things are:

  1. UrlMkSetSessionOption works for WebBrowser (=IE) but does not for WebViewControl (=Edge)
  2. WebView2 doesn't have an API for it, but it is possible to set it via the options during creation (quite easy to implement)
  3. webkit2gtk offers API
  4. WKWebView offers API

I might be able to make things a bit easier by adding a small utility class that downloads the webview installer and executes it for you (with permission of the user obviously). It'd be like described in the linked issue and wouldn't incur any penalty in app size or require an installer to be shipped for the app. It would require an internet connection though.

That sounds great, and I think any user on Windows should ultimately be on WebView2, not on WebBrowser or WebViewControl, though especially in corporate environments that might be a bit difficult in the beginning. I think it's fair to say that we can just defer that all until the linked issue is resolved and WebView2 settles on their exact API/regkey details there (soon hopefully).

Your linked SO answer is similar to the proxy solution yes, it does however only work on the page request and not on any subsequent resource requests (js, css, images etc.).

That's because the workaround is only acting on the NavigationStarting event, right? That's perhaps good enough for most use cases - it definitely is good enough for mine. I also don't see any way to handle anything further:

  • The WebResourceRequested event fires after the request has been made already. You can adjust the response, though... but that doesn't help in most scenarios, besides it's probably a bad idea to just mindlessly repeat the same original request with changed headers 😆
  • ContentLoading's WebViewContentLoadingEventArgs class doesn't allow canceling or replacing the original request with our own.

I'll try to include methods that make it as easy to use as possible so you just have to change/add headers and the rest of the request is handled for you.

If that proxy solution is available for all the platforms that would be way more powerful than my initial request of just setting the user agent, so that would be great if it's possible!

The sad part about the state of all components is the fact that WebViewControl is the one that my users are using most of the time, yet it's the only one giving this issue of "just" setting the user agent header. Fingers crossed for the proxy solution. Is there a point in continuing the discussion/your findings in a new issue for it? Happy to just follow along in here or by watching for commits (and also helping out if I can)!