/* Copyright (c) Citrix Systems Inc. * All rights reserved. * * Redistribution and use in source and binary forms, * with or without modification, are permitted provided * that the following conditions are met: * * * Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the * following disclaimer in the documentation and/or other * materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Security.Permissions; using System.Windows.Forms; namespace XenAdmin.Core { /// /// This extension to System.Windows.Forms.WebBrowser adds two capabilities: an event fired whenever /// browser navigation fails (NavigateError), and an event fired whenever the browser wants to /// prompt for credentials (AuthenticationPrompt). /// /// The event sink for NavigateError comes from /// http://msdn.microsoft.com/en-us/library/system.windows.forms.webbrowser.createsink(VS.80).aspx. /// The handling of authentication prompts comes partially from /// http://izlooite.blogspot.com/2009/06/bypass-integrated-authentication-using.html. /// The bit that he missed is that we can override CreateWebBrowserSiteBase to get ourselves /// registered rather than having to override IOleClientSite as well. See /// http://msdn2.microsoft.com/en-us/library/system.windows.forms.webbrowser.createwebbrowsersitebase.aspx. /// /// (Final note: Contrary to the docs, you can't use the CreateWebBrowserSiteBase override to reimplement /// IDocHostUIHandler. This is an acknowledged Microsoft bug /// https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=115198. /// Fortunately, we only need to implement IAuthenticate, not IDocHostUIHandler, so this isn't a problem /// for us, but it was confusing for a couple of days.) /// public class WebBrowser2 : WebBrowser { /// /// A list of s used to determine whether URLS are valid before Navigations. /// private readonly List _webClients = new List(); private AxHost.ConnectionPointCookie cookie; private WebBrowser2EventHelper helper; public event WebBrowserNavigateErrorEventHandler NavigateError; public event EventHandler WindowClosed; public event WebBrowserAuthenticationPromptEventHandler AuthenticationPrompt; protected bool Authenticate(out string username, out string password) { if (AuthenticationPrompt == null) { username = ""; password = ""; return false; } else { WebBrowserAuthenticationPromptEventArgs args = new WebBrowserAuthenticationPromptEventArgs(); AuthenticationPrompt(this, args); username = args.Username; password = args.Password; return args.Success; } } [PermissionSet(SecurityAction.LinkDemand, Name="FullTrust")] protected override void CreateSink() { base.CreateSink(); // Create an instance of the client that will handle the event // and associate it with the underlying ActiveX control. helper = new WebBrowser2EventHelper(this); cookie = new AxHost.ConnectionPointCookie( this.ActiveXInstance, helper, typeof(DWebBrowserEvents2)); } [PermissionSet(SecurityAction.LinkDemand, Name="FullTrust")] protected override void DetachSink() { // Disconnect the client that handles the event // from the underlying ActiveX control. if (cookie != null) { cookie.Disconnect(); cookie = null; } base.DetachSink(); } protected override WebBrowserSiteBase CreateWebBrowserSiteBase() { return new WebBrowserSite2(this); } protected virtual void OnNavigateError(WebBrowserNavigateErrorEventArgs e) { if (NavigateError != null) NavigateError(this, e); } protected override void WndProc(ref Message m) { switch (m.Msg) { case Win32.WM_PARENTNOTIFY: if (!DesignMode && (int)m.WParam == Win32.WM_DESTROY) { if (WindowClosed != null) WindowClosed(this, EventArgs.Empty); } break; default: base.WndProc(ref m); break; } } protected override void OnNavigating(WebBrowserNavigatingEventArgs e) { Program.AssertOnEventThread(); // clear the _webClients so that an existing multiple-url Navigation is cancelled. _webClients.Clear(); base.OnNavigating(e); } /// /// Navigates to the specified URI. /// public new void Navigate(Uri uri) { Program.AssertOnEventThread(); Navigate(uri, null, null, "X-XenCenter: " + Program.ClientVersion()); } /// /// Navigates to the first valid URI in the specified list. /// public void Navigate(IEnumerable uris) { Program.AssertOnEventThread(); Util.ThrowIfEnumerableParameterNullOrEmpty(uris, "uris"); List uriList = new List(uris); if (uriList.Count == 1) { Navigate(uriList[0]); return; } // test each url with a WebClient to see if it works. _webClients.Clear(); _webClients.AddRange(uriList.ConvertAll(u => new WebClient())); // start all urls downloading in parallel. for (int i = 0; i < _webClients.Count; i++) { _webClients[i].DownloadDataCompleted += webClient_DownloadDataCompleted; try { _webClients[i].DownloadDataAsync(uriList[i], uriList[i]); } catch (WebException) { // we are expecting some urls to fail: do nothing. } catch (SocketException) { // we are expecting some urls to fail: do nothing. } } } private void webClient_DownloadDataCompleted(object sender, DownloadDataCompletedEventArgs e) { Program.AssertOnEventThread(); WebClient webClient = (WebClient)sender; if (_webClients.Contains(webClient)) { _webClients.Remove(webClient); if (e.Error == null || (e.Error != null && _webClients.Count == 0)) { // either one has finished successfully...or...they've all failed. // navigate the browser to this url and leave other requests (if any) to timeout. _webClients.Clear(); Navigate((Uri)e.UserState); } } else { // either a valid url has been found...or another Navigate has started: do nothing. } } private class WebBrowserSite2 : WebBrowserSite, Win32.IAuthenticate, Win32.IServiceProvider { private WebBrowser2 Browser; public WebBrowserSite2(WebBrowser2 Browser) : base(Browser) { this.Browser = Browser; } #region IAuthenticate Members public int Authenticate(ref IntPtr phwnd, ref IntPtr pszUsername, ref IntPtr pszPassword) { string username, password; if (Browser.Authenticate(out username, out password)) { IntPtr sUser = Marshal.StringToCoTaskMemAuto(username); IntPtr sPassword = Marshal.StringToCoTaskMemAuto(password); pszUsername = sUser; pszPassword = sPassword; return Win32.S_OK; } else { return Win32.E_ACCESSDENIED; } } #endregion #region IServiceProvider Members public int QueryService(ref Guid guidService, ref Guid riid, out IntPtr ppvObject) { if (guidService.CompareTo(Win32.IID_IAuthenticate) == 0 && riid.CompareTo(Win32.IID_IAuthenticate) == 0) { ppvObject = Marshal.GetComInterfaceForObject(this, typeof(Win32.IAuthenticate)); return Win32.S_OK; } else { ppvObject = IntPtr.Zero; return Win32.INET_E_DEFAULT_ACTION; } } #endregion } // Handles the NavigateError event from the underlying ActiveX // control by raising the NavigateError event defined in this class. private class WebBrowser2EventHelper : StandardOleMarshalObject, DWebBrowserEvents2 { private WebBrowser2 parent; public WebBrowser2EventHelper(WebBrowser2 parent) { this.parent = parent; } public void NavigateError(object pDisp, ref object url, ref object frame, ref object statusCode, ref bool cancel) { parent.OnNavigateError( new WebBrowserNavigateErrorEventArgs( (string)url, (string)frame, (Int32)statusCode, cancel)); } } } public delegate void WebBrowserNavigateErrorEventHandler(object sender, WebBrowserNavigateErrorEventArgs e); public class WebBrowserNavigateErrorEventArgs : EventArgs { public string Url; public string Frame; public Int32 StatusCode; public bool Cancel; public WebBrowserNavigateErrorEventArgs(string url, string frame, Int32 statusCode, bool cancel) { Url = url; Frame = frame; StatusCode = statusCode; Cancel = cancel; } } public delegate void WebBrowserAuthenticationPromptEventHandler(WebBrowser2 sender, WebBrowserAuthenticationPromptEventArgs e); public class WebBrowserAuthenticationPromptEventArgs { /// /// Set to true by the event handler if the user clicked OK on the authentication prompt. /// public bool Success; /// /// Set by the event handler, iff Success is true. /// public string Username; /// /// Set by the event handler, iff Success is true. /// public string Password; } [ComImport, Guid("34A715A0-6587-11D0-924A-0020AFC7AC4D"), InterfaceType(ComInterfaceType.InterfaceIsIDispatch), TypeLibType(TypeLibTypeFlags.FHidden)] public interface DWebBrowserEvents2 { [DispId(271)] void NavigateError([In, MarshalAs(UnmanagedType.IDispatch)] object pDisp, [In] ref object URL, [In] ref object frame, [In] ref object statusCode, [In, Out] ref bool cancel); } }