2017-01-16 20:59:50 +01:00
/ * Copyright ( c ) Citrix Systems , Inc .
2013-06-24 13:41:48 +02:00
* 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.IO ;
using System.Net ;
using System.Resources ;
using System.Runtime.InteropServices ;
using System.Text ;
using System.Text.RegularExpressions ;
using System.Threading ;
using System.Windows.Forms ;
using System.Xml ;
using XenAdmin.Core ;
using XenAdmin.Network ;
using XenAPI ;
namespace XenAdmin.Plugins
{
/// <summary>
/// Class describing a plugin feature which loads a URL to display as an extra tab inside XenCenter; this can be used
/// to allow access to web management consoles or to add additional user interface features into XenCenter.
/// </summary>
internal class TabPageFeature : Feature
{
/ *
* Each TabPageFeature has an associated WebBrowser2 instance . There is just one per feature , and therefore they are shared if a
* feature appears on multiple XenObjects . Each time the user switches XenObject , the state of the browser for that particular
* XenObject is persisted in BrowserStates .
*
*
* We have a fairly complicated scheme for managing credentials for webpages :
*
* Normally , when you hit a page that requires authorization , IE will pop up a prompt . We intercept this prompt , and instead we
* persist credentials on the XenServer host . We also prompt if we see HTTP response 401 Authorization Required or 403 Forbidden ,
* on the assumption that the password on the server has changed .
*
* Credentials are persisted server - side as Secrets , with a reference to the appropriate Secret stored in Pool . gui_config ( see
* Pool . * XCPluginSecret ) . If the user chooses not to persist credentials , we still persist an empty string in Pool . gui_config ,
* to record the fact that the user has made that choice .
*
* When we see an authentication request , we enter Browser_AuthenticationPrompt on the UI thread . We check the server for
* persisted credentials , which has to occur on a background thread , but we can ' t return from Browser_AuthenticationPrompt until
* we have the credentials . To handle this , we spawn a thread in TriggerGetSecret , and then use Application . DoEvents to wait on
* the UI thread without blocking redraws . See CompleteGetSecret . The background thread will set BrowserState . Credentials when
* it is finished , releasing the UI thread .
*
* If there ' s nothing in Pool . gui_config for us , or if there is the empty string ( indicating that the user has chosen not to
* persist credentials ) , then we need to prompt for the credentials from the user . In that case , it ' s back onto the UI thread
* to show a TabPageCredentialsDialog before returning to the background thread to persist the new credentials .
* /
2018-08-02 16:52:13 +02:00
[DllImport("wininet.dll", SetLastError = true)]
private static extern long DeleteUrlCacheEntry ( string url ) ;
#region Fields
2013-06-24 13:41:48 +02:00
private static readonly log4net . ILog log = log4net . LogManager . GetLogger ( System . Reflection . MethodBase . GetCurrentMethod ( ) . DeclaringType ) ;
private const char CREDENTIALS_SEPARATOR = ' \ x0294 ' ;
2018-08-02 16:52:13 +02:00
/// <summary>
/// required - "url" attribute, the local or remote url of the HTML page to load
/// </summary>
private readonly string Url ;
/// <summary>
/// optional - "context-menu" attribute, if set the context menu will be disabled
/// </summary>
private readonly bool ContextEnabled ;
/// <summary>
/// optional - "xencenter-only" attribute, if set this tabpage will appear on the XenCenter node and no where else
/// </summary>
private readonly bool XenCenterOnly ;
/// <summary>
/// optional - "relative" attribute, if set the url will be resolved relative to the XenCenter directory
/// </summary>
private readonly bool RelativeUrl ;
/// <summary>
/// optional - "help-link" attribute, if set when the help button is pressed this url will be loaded in a separate browser
/// </summary>
private readonly string HelpLink ;
/// <summary>
/// optional - "credentials" attribute, if set XenCenter will duplicate a session for use by the htmls JavaScript
/// </summary>
private readonly bool Credentials ;
/// <summary>
/// optional - "console" attribute on the "TabPage" tag.
/// Indicates that this tab-page is a replacement for the console tab.
/// </summary>
private readonly bool Console ;
2013-06-24 13:41:48 +02:00
public const string ELEMENT_NAME = "TabPage" ;
public const string ATT_XC_ONLY = "xencenter-only" ;
public const string ATT_CONTEXT_MENU = "context-menu" ;
public const string ATT_HELP_LINK = "help-link" ;
public const string ATT_RELATIVE = "relative" ;
public const string ATT_CREDENTIALS = "credentials" ;
public const string ATT_CONSOLE = "console" ;
public const string ATT_URL = "url" ;
private readonly Dictionary < IXenObject , BrowserState > BrowserStates = new Dictionary < IXenObject , BrowserState > ( ) ;
2018-08-06 17:48:39 +02:00
private BrowserState lastBrowserState ;
2013-06-24 13:41:48 +02:00
2018-08-06 17:48:39 +02:00
private StatusStrip statusStrip ;
private ToolStripStatusLabel statusLabel ;
2013-06-24 13:41:48 +02:00
private TabPage _tabPage ;
private WebBrowser2 Browser ;
2018-08-02 16:52:13 +02:00
#endregion
public TabPageFeature ( ResourceManager resourceManager , XmlNode node , PluginDescriptor plugin )
: base ( resourceManager , node , plugin )
{
XenCenterOnly = Helpers . GetBoolXmlAttribute ( node , ATT_XC_ONLY , false ) ;
ContextEnabled = Helpers . GetBoolXmlAttribute ( node , ATT_CONTEXT_MENU , false ) ;
// plugins v2
HelpLink = Helpers . GetStringXmlAttribute ( node , ATT_HELP_LINK , "" ) ;
RelativeUrl = Helpers . GetBoolXmlAttribute ( node , ATT_RELATIVE , false ) ;
Credentials = Helpers . GetBoolXmlAttribute ( node , ATT_CREDENTIALS , false ) ;
Console = Helpers . GetBoolXmlAttribute ( node , ATT_CONSOLE , false ) ;
string urlString = Helpers . GetStringXmlAttribute ( node , ATT_URL ) ;
Url = urlString = = null ? "" : string . Format ( "{0}{1}" , RelativeUrl ? string . Format ( "{0}/" , Application . StartupPath ) : "" , urlString ) ;
}
#region Properties
2013-06-24 13:41:48 +02:00
public bool HasHelp
{
2018-08-02 16:52:13 +02:00
get { return ! string . IsNullOrEmpty ( HelpLink ) ; }
2013-06-24 13:41:48 +02:00
}
public TabPage TabPage
{
2018-08-02 16:52:13 +02:00
get { return _tabPage ; }
2013-06-24 13:41:48 +02:00
}
2018-08-06 19:42:16 +02:00
public IXenObject SelectedXenObject { get ; set ; }
2013-06-24 13:41:48 +02:00
2018-08-02 16:52:13 +02:00
/// <summary>
/// Gets a value indicating whether this instance should replace the console tab.
/// </summary>
public bool IsConsoleReplacement
2013-06-24 13:41:48 +02:00
{
2018-08-02 16:52:13 +02:00
get
{
return Console ;
}
}
2013-06-24 13:41:48 +02:00
2018-08-02 16:52:13 +02:00
/// <summary>
/// Gets a value indicating whether the most recent navigation for the selected xen object
/// resulted in an error.
/// </summary>
public bool IsError
{
get
{
2018-08-06 12:16:05 +02:00
if ( SelectedXenObject ! = null & & BrowserStates . ContainsKey ( SelectedXenObject ) )
return BrowserStates [ SelectedXenObject ] . IsError ;
2018-08-02 16:52:13 +02:00
2018-08-06 12:16:05 +02:00
return false ;
2018-08-02 16:52:13 +02:00
}
2013-06-24 13:41:48 +02:00
}
2018-08-02 16:52:13 +02:00
#endregion
2013-06-24 13:41:48 +02:00
public override string CheckForError ( )
{
if ( Url = = "" )
return string . Format ( Messages . CANNOT_PARSE_NODE_PARAM , node . Name , ATT_URL ) ;
return base . CheckForError ( ) ;
}
public override void Initialize ( )
{
_tabPage = new TabPage ( ) ;
2018-08-06 17:48:39 +02:00
try
{
_tabPage . SuspendLayout ( ) ;
_tabPage . Text = ToString ( ) ;
_tabPage . ToolTipText = Tooltip ? ? "" ;
if ( Program . MainWindow ! = null ) // for unit tests
_tabPage . HelpRequested + = Program . MainWindow . MainWindow_HelpRequested ;
2013-06-24 13:41:48 +02:00
2018-08-06 17:48:39 +02:00
CreateStatusBar ( ) ;
CreateBrowser ( ) ;
_tabPage . Controls . Add ( statusStrip ) ;
_tabPage . Controls . Add ( Browser ) ;
Browser . BringToFront ( ) ;
_tabPage . Tag = this ; // Tag the tab page so we can reload when the tab is focused.
}
finally
2013-06-24 13:41:48 +02:00
{
2018-08-06 17:48:39 +02:00
_tabPage . ResumeLayout ( ) ;
2013-06-24 13:41:48 +02:00
}
2018-08-06 17:48:39 +02:00
}
2013-06-24 13:41:48 +02:00
2018-08-06 17:48:39 +02:00
private void CreateStatusBar ( )
{
statusLabel = new ToolStripStatusLabel
{
Overflow = ToolStripItemOverflow . Never ,
Spring = true ,
TextAlign = System . Drawing . ContentAlignment . MiddleLeft ,
} ;
statusStrip = new StatusStrip
{
SizingGrip = false ,
AutoSize = false ,
Items = { statusLabel } ,
Visible = false
} ;
2013-06-24 13:41:48 +02:00
}
private void CreateBrowser ( )
{
2018-08-06 17:48:39 +02:00
Browser = new WebBrowser2 { Dock = DockStyle . Fill , IsWebBrowserContextMenuEnabled = ContextEnabled } ;
2013-11-12 13:44:19 +01:00
Browser . ProgressChanged + = Browser_ProgressChanged ;
2013-06-24 13:41:48 +02:00
Browser . DocumentCompleted + = Browser_DocumentCompleted ;
Browser . Navigating + = Browser_Navigating ;
Browser . Navigated + = Browser_Navigated ;
Browser . NavigateError + = Browser_NavigateError ;
Browser . WindowClosed + = Browser_WindowClosed ;
Browser . PreviewKeyDown + = Browser_PreviewKeyDown ;
Browser . AuthenticationPrompt + = Browser_AuthenticationPrompt ;
// Navigate to about:blank to work around http://support.microsoft.com/kb/320153.
Browser . Navigate ( "about:blank" ) ;
}
2018-08-02 16:52:13 +02:00
2013-06-24 13:41:48 +02:00
public bool ShowTab
{
get
{
try
{
2018-08-06 12:16:05 +02:00
if ( _tabPage = = null )
2013-06-24 13:41:48 +02:00
return false ;
if ( XenCenterOnly )
return SelectedXenObject = = null ;
if ( SelectedXenObject = = null | | ! Enabled | | ! Placeholders . UriValid ( Url , SelectedXenObject ) )
return false ;
return Search = = null | | Search . Query . Match ( SelectedXenObject ) ;
}
catch ( UriFormatException e )
{
log . Debug ( string . Format ( "Not displaying tab '{0}' for plugin '{1}'. Invalid properties in url '{2}'" , ToString ( ) , PluginDescriptor . Name , Url ) , e ) ;
2018-08-06 12:16:05 +02:00
return false ;
2013-06-24 13:41:48 +02:00
}
}
}
2018-08-06 12:16:05 +02:00
public void SetUrl ( )
2013-06-24 13:41:48 +02:00
{
2018-08-06 17:48:39 +02:00
// Never update XenCenter node tabs once loaded
if ( Browser . Url ! = null & & Browser . Url . AbsoluteUri ! = "about:blank" & & XenCenterOnly )
2013-06-24 13:41:48 +02:00
return ;
BrowserState state ;
2018-08-06 12:16:05 +02:00
if ( SelectedXenObject = = null )
2013-06-24 13:41:48 +02:00
{
// XenCenter node is selected, the placeholder code will sub in "null" for all placeholders
// After this point we will never update this url again for this node, so there is no need to store a browser state
2018-08-06 12:16:05 +02:00
state = new BrowserState ( Placeholders . SubstituteUri ( Url , SelectedXenObject ) , SelectedXenObject , Browser ) ;
2013-06-24 13:41:48 +02:00
}
2018-08-06 17:48:39 +02:00
else if ( BrowserStates . ContainsKey ( SelectedXenObject ) )
2013-06-24 13:41:48 +02:00
{
2018-08-06 12:16:05 +02:00
state = BrowserStates [ SelectedXenObject ] ;
2018-08-06 17:48:39 +02:00
state . Uris = Placeholders . SubstituteUri ( Url , SelectedXenObject ) ;
2013-06-24 13:41:48 +02:00
}
else
{
2018-08-06 12:16:05 +02:00
state = new BrowserState ( Placeholders . SubstituteUri ( Url , SelectedXenObject ) , SelectedXenObject , Browser ) ;
BrowserStates [ SelectedXenObject ] = state ;
2013-06-24 13:41:48 +02:00
}
try
{
if ( state . ObjectForScripting ! = null )
{
if ( Credentials )
state . ObjectForScripting . LoginSession ( ) ;
Browser . ObjectForScripting = state . ObjectForScripting ;
}
lastBrowserState = state ;
2018-08-06 17:48:39 +02:00
Browser . DocumentText = string . Empty ;
Application . DoEvents ( ) ;
lastBrowserState . IsError = false ;
ShowStatus ( string . Format ( Messages . WEB_BROWSER_WAITING , ShortUri ( state . Uris [ 0 ] ) ) ) ;
Browser . Navigate ( state . Uris [ 0 ] ) ;
2013-06-24 13:41:48 +02:00
}
catch ( Exception e )
{
2018-08-06 17:48:39 +02:00
log . Error ( string . Format ( "Failed to set TabPage url to '{0}' for plugin '{1}'" ,
string . Join ( "," , state . Uris . ConvertAll ( u = > u . ToString ( ) ) . ToArray ( ) ) , PluginDescriptor . Name ) , e ) ;
2013-06-24 13:41:48 +02:00
}
}
2018-08-06 17:48:39 +02:00
private void ShowStatus ( string text )
{
statusLabel . Text = text ;
statusStrip . Visible = true ;
}
private void HideStatus ( )
{
statusStrip . Visible = false ;
}
private string ShortUri ( Uri uri )
{
return uri . ToString ( ) . Ellipsise ( 80 ) ;
}
2018-08-02 16:52:13 +02:00
#region WebBrowser2 event handlers
2013-06-24 13:41:48 +02:00
private void Browser_Navigating ( object sender , WebBrowserNavigatingEventArgs e )
{
Program . AssertOnEventThread ( ) ;
2018-08-06 17:48:39 +02:00
if ( XenCenterOnly | | lastBrowserState = = null )
2013-06-24 13:41:48 +02:00
return ;
2018-08-06 17:48:39 +02:00
log . DebugFormat ( "Navigating to {0} for {1}" , e . Url , Helpers . GetName ( lastBrowserState . Obj ) ) ;
DeleteUrlCacheEntry ( e . Url . AbsoluteUri ) ;
2013-06-24 13:41:48 +02:00
if ( Console )
{
// delete this page from the cache.... that we can be sure that if the page stops working then
// the user reliably gets the real console back.
DeleteUrlCacheEntry ( e . Url . AbsoluteUri ) ;
}
}
2018-08-02 17:04:16 +02:00
private void Browser_NavigateError ( object sender , WebBrowser2 . NavigateErrorEventArgs e )
2013-06-24 13:41:48 +02:00
{
Program . AssertOnEventThread ( ) ;
2018-08-06 17:48:39 +02:00
lastBrowserState . IsError = true ;
log . DebugFormat ( "Got error {0} while navigating to {1}." , e . StatusCode , e . Url ) ;
2013-06-24 13:41:48 +02:00
try
{
2018-08-06 17:48:39 +02:00
if ( e . StatusCode = = 401 | | e . StatusCode = = 403 )
2013-06-24 13:41:48 +02:00
{
2018-08-06 17:48:39 +02:00
log . Debug ( "Clearing secret and re-prompting." ) ;
2013-06-24 13:41:48 +02:00
CompleteClearSecret ( lastBrowserState ) ;
TriggerGetSecret ( lastBrowserState ) ;
}
}
catch ( Exception exn )
{
log . Error ( exn , exn ) ;
}
}
2018-08-02 16:52:13 +02:00
private void Browser_Navigated ( object sender , WebBrowserNavigatedEventArgs e )
2013-06-24 13:41:48 +02:00
{
2018-08-06 17:48:39 +02:00
if ( XenCenterOnly | | lastBrowserState = = null )
2013-06-24 13:41:48 +02:00
{
2018-08-06 17:48:39 +02:00
HideStatus ( ) ;
return ;
}
log . DebugFormat ( "Navigated to {0} for {1}" , e . Url , Helpers . GetName ( lastBrowserState . Obj ) ) ;
2013-06-24 13:41:48 +02:00
2018-08-06 17:48:39 +02:00
if ( lastBrowserState . IsError & & e . Url ! = null & & e . Url . AbsoluteUri ! = "about:blank" )
{
if ( Console )
2018-08-02 16:52:13 +02:00
{
2018-08-06 17:48:39 +02:00
// if this plugin tab-page is a console replacement and the error-state of the navigation has changed
// then update the tabs. This ensures that user gets the real console tab back.
Program . MainWindow . UpdateToolbars ( ) ;
}
2013-06-24 13:41:48 +02:00
2018-08-06 17:48:39 +02:00
lastBrowserState . Uris . Remove ( e . Url ) ;
if ( lastBrowserState . Uris . Count > 0 )
{
lastBrowserState . IsError = false ;
ShowStatus ( string . Format ( Messages . WEB_BROWSER_FAILED_RETRYING , ShortUri ( e . Url ) ,
ShortUri ( lastBrowserState . Uris [ 0 ] ) ) ) ;
Browser . Navigate ( lastBrowserState . Uris [ 0 ] ) ;
}
else
{
ShowStatus ( string . Format ( Messages . WEB_BROWSER_FAILED , ShortUri ( e . Url ) ) ) ;
Browser . Navigate ( "about:blank" ) ;
2018-08-02 16:52:13 +02:00
}
2013-06-24 13:41:48 +02:00
}
}
2018-08-02 16:52:13 +02:00
private void Browser_ProgressChanged ( object sender , WebBrowserProgressChangedEventArgs e )
2013-06-24 13:41:48 +02:00
{
2018-08-02 16:52:13 +02:00
Program . AssertOnEventThread ( ) ;
2018-08-06 17:48:39 +02:00
if ( XenCenterOnly | | lastBrowserState = = null | | lastBrowserState . Uris . Count = = 0 | |
Browser . ReadyState = = WebBrowserReadyState . Complete )
return ;
2013-06-24 13:41:48 +02:00
2018-08-06 17:48:39 +02:00
if ( e . MaximumProgress > 0 & & e . CurrentProgress > = 0 & & e . CurrentProgress < e . MaximumProgress )
2013-06-24 13:41:48 +02:00
{
2018-08-06 17:48:39 +02:00
int progr = Convert . ToInt32 ( e . CurrentProgress * 100L / e . MaximumProgress ) ;
if ( progr > 100 )
progr = 100 ;
ShowStatus ( string . Format ( Messages . WEB_BROWSER_LOADING_PERCENT , ShortUri ( lastBrowserState . Uris [ 0 ] ) , progr ) ) ;
}
else
{
ShowStatus ( string . Format ( Messages . WEB_BROWSER_LOADING , ShortUri ( lastBrowserState . Uris [ 0 ] ) ) ) ;
2013-06-24 13:41:48 +02:00
}
}
2018-08-02 16:52:13 +02:00
private void Browser_DocumentCompleted ( object sender , WebBrowserDocumentCompletedEventArgs e )
2013-06-24 13:41:48 +02:00
{
2018-08-02 16:52:13 +02:00
Program . AssertOnEventThread ( ) ;
2018-08-06 17:48:39 +02:00
if ( XenCenterOnly | | lastBrowserState = = null | | lastBrowserState . IsError )
return ;
HideStatus ( ) ;
2013-06-24 13:41:48 +02:00
}
private void Browser_PreviewKeyDown ( object sender , PreviewKeyDownEventArgs e )
{
if ( e . KeyCode = = Keys . F1 & & Browser . Focused )
{
Program . MainWindow . MainWindow_HelpRequested ( null , null ) ;
}
}
2018-08-02 17:04:16 +02:00
private void Browser_AuthenticationPrompt ( WebBrowser2 sender , WebBrowser2 . AuthenticationPromptEventArgs e )
2013-06-24 13:41:48 +02:00
{
try
{
Program . AssertOnEventThread ( ) ;
log . Debug ( "Prompting for authentication..." ) ;
CompleteClearSecret ( lastBrowserState ) ;
CompleteGetSecret ( lastBrowserState ) ;
BrowserState . BrowserCredentials creds = lastBrowserState . Credentials ;
if ( creds ! = null & & creds . Valid )
{
e . Username = creds . Username ;
e . Password = creds . Password ;
e . Success = true ;
log . Debug ( "Prompt for authentication successful." ) ;
}
else
{
e . Success = false ;
log . Debug ( "Prompt for authentication cancelled / failed." ) ;
}
}
catch ( Exception exn )
{
log . Error ( "Prompt for authentication failed" , exn ) ;
e . Success = false ;
}
}
2018-08-02 16:52:13 +02:00
private void Browser_WindowClosed ( object sender , EventArgs e )
{
if ( ! Program . Exiting & & Enabled & & ! _tabPage . Disposing )
{
_tabPage . Controls . Remove ( Browser ) ;
CreateBrowser ( ) ;
_tabPage . Controls . Add ( Browser ) ;
if ( SelectedXenObject ! = null )
{
BrowserStates . Remove ( SelectedXenObject ) ;
SetUrl ( ) ;
}
}
}
#endregion
2013-06-24 13:41:48 +02:00
private void CompleteGetSecret ( BrowserState state )
{
if ( state . Credentials ! = null )
return ;
TriggerGetSecret ( state ) ;
while ( state . Credentials = = null )
Application . DoEvents ( ) ;
}
/// <summary>
/// Load the secret where we've saved credentials. This will be placed on a background thread, and we'll poll for
/// its results.
/// </summary>
private void TriggerGetSecret ( BrowserState state )
{
Thread t = new Thread ( GetSecret ) ;
t . IsBackground = true ;
t . Start ( state ) ;
}
/// <summary>
/// Get the persisted secret from the server, or prompt for new credentials and persist those back to the server.
/// Completion of this thread is indicated by state.Credentials being set.
/// </summary>
/// <param name="obj"></param>
private void GetSecret ( object obj )
{
Program . AssertOffEventThread ( ) ;
BrowserState state = ( BrowserState ) obj ;
try
{
Session session = state . Obj . Connection . Session ;
do
{
Pool pool = Helpers . GetPoolOfOne ( state . Obj . Connection ) ;
if ( pool = = null )
{
log . Warn ( "Failed to get Pool!" ) ;
Thread . Sleep ( 5000 ) ;
continue ;
}
string secret_uuid = pool . GetXCPluginSecret ( PluginDescriptor . Name , state . Obj ) ;
2018-08-02 18:13:28 +02:00
if ( string . IsNullOrEmpty ( secret_uuid ) )
2013-06-24 13:41:48 +02:00
{
2018-08-02 18:13:28 +02:00
var msg = secret_uuid = = null ? "Nothing persisted." : "User chose not to persist these credentials." ;
log . Debug ( msg + " Prompting for new credentials." ) ;
Program . Invoke ( Program . MainWindow , ( ) = > { state . Credentials = PromptForUsernamePassword ( secret_uuid = = null ) ; } ) ;
MaybePersistCredentials ( session , pool , state . Obj , state . Credentials ) ;
2013-06-24 13:41:48 +02:00
return ;
}
else
{
log . Debug ( "Found a secret." ) ;
XenRef < Secret > secret = null ;
try
{
secret = Secret . get_by_uuid ( session , secret_uuid ) ;
}
catch ( Failure exn )
{
2018-08-02 18:13:28 +02:00
log . Warn ( string . Format ( "Secret {0} for {1} on plugin {2} has disappeared! Removing from pool.gui_config." ,
secret_uuid , Helpers . GetName ( state . Obj ) , PluginDescriptor . Name ) , exn ) ;
2013-06-24 13:41:48 +02:00
TryToRemoveSecret ( pool , session , PluginDescriptor . Name , state . Obj ) ;
continue ;
}
string val = Secret . get_value ( session , secret ) ;
string [ ] bits = val . Split ( CREDENTIALS_SEPARATOR ) ;
if ( bits . Length ! = 2 )
{
log . WarnFormat ( "Corrupt secret {0} at {1} for {2} on plugin {3}! Deleting." , val , secret_uuid ,
Helpers . GetName ( state . Obj ) , PluginDescriptor . Name ) ;
TryToDestroySecret ( session , secret . opaque_ref ) ;
TryToRemoveSecret ( pool , session , PluginDescriptor . Name , state . Obj ) ;
continue ;
}
log . Debug ( "Secret successfully read." ) ;
2018-08-02 18:13:28 +02:00
state . Credentials = new BrowserState . BrowserCredentials
{
Username = bits [ 0 ] ,
Password = bits [ 1 ] ,
PersistCredentials = true ,
Valid = true
} ;
return ;
2013-06-24 13:41:48 +02:00
}
// Unreachable. Should either have returned, or continued (to retry).
} while ( true ) ;
}
catch ( Exception exn )
{
log . Warn ( "Ignoring exception when trying to get secret" , exn ) ;
2018-08-02 18:13:28 +02:00
// Note that it's essential that we set state.Credentials before leaving this function,
// because other threads are waiting for that value to appear.
state . Credentials = new BrowserState . BrowserCredentials { Valid = false } ;
2013-06-24 13:41:48 +02:00
}
}
2018-08-02 18:13:28 +02:00
private void MaybePersistCredentials ( Session session , Pool pool , IXenObject obj , BrowserState . BrowserCredentials creds )
2013-06-24 13:41:48 +02:00
{
if ( creds ! = null & & creds . Valid )
{
2018-08-02 18:13:28 +02:00
string secretUuid = creds . PersistCredentials ? CreateSecret ( session , creds . Username , creds . Password ) : "" ;
pool . SetXCPluginSecret ( session , PluginDescriptor . Name , obj , secretUuid ) ;
2013-06-24 13:41:48 +02:00
}
}
private string CreateSecret ( Session session , string username , string password )
{
string val = string . Format ( "{0}{1}{2}" , username , CREDENTIALS_SEPARATOR , password ) ;
return Secret . CreateSecret ( session , val ) ;
}
2018-08-02 18:13:28 +02:00
private BrowserState . BrowserCredentials PromptForUsernamePassword ( bool persistCredentials )
2013-06-24 13:41:48 +02:00
{
Program . AssertOnEventThread ( ) ;
2018-08-02 18:13:28 +02:00
using ( var d = new TabPageCredentialsDialog { ServiceName = Label , PersistCredentials = persistCredentials } )
2013-06-24 13:41:48 +02:00
{
2018-08-02 18:13:28 +02:00
if ( d . ShowDialog ( Program . MainWindow ) = = DialogResult . OK )
{
return new BrowserState . BrowserCredentials
{
Username = d . Username ,
Password = d . Password ,
PersistCredentials = d . PersistCredentials ,
Valid = true
} ;
}
return new BrowserState . BrowserCredentials { Valid = false } ;
2013-06-24 13:41:48 +02:00
}
}
private void CompleteClearSecret ( BrowserState state )
{
if ( state . Credentials = = null )
return ;
TriggerClearSecret ( state ) ;
while ( state . Credentials ! = null )
Application . DoEvents ( ) ;
}
private void TriggerClearSecret ( BrowserState state )
{
Thread t = new Thread ( ClearSecret ) ;
t . IsBackground = true ;
t . Start ( state ) ;
}
/// <summary>
/// Clear the persisted secret from the server.
/// Completion of this thread is indicated by state.Credentials being set to null.
/// </summary>
private void ClearSecret ( object obj )
{
Program . AssertOffEventThread ( ) ;
BrowserState state = ( BrowserState ) obj ;
try
{
Session session = state . Obj . Connection . Session ;
Pool pool = Helpers . GetPoolOfOne ( state . Obj . Connection ) ;
if ( pool = = null )
return ;
string secret_uuid = pool . GetXCPluginSecret ( PluginDescriptor . Name , state . Obj ) ;
2013-09-25 12:10:28 +02:00
if ( ! string . IsNullOrEmpty ( secret_uuid ) )
2013-06-24 13:41:48 +02:00
{
XenRef < Secret > secret_ref = Secret . get_by_uuid ( session , secret_uuid ) ;
TryToDestroySecret ( session , secret_ref . opaque_ref ) ;
TryToRemoveSecret ( pool , session , PluginDescriptor . Name , state . Obj ) ;
}
}
catch ( Exception exn )
{
log . Warn ( "Ignoring exception when trying to clear secret" , exn ) ;
}
state . Credentials = null ;
}
private static void TryToDestroySecret ( Session session , string opaque_ref )
{
try
{
Secret . destroy ( session , opaque_ref ) ;
log . DebugFormat ( "Successfully removed secret {0}" , opaque_ref ) ;
}
catch ( Exception exn )
{
log . Error ( string . Format ( "Failed to destroy secret {0}" , opaque_ref ) , exn ) ;
}
}
private static void TryToRemoveSecret ( Pool pool , Session session , string name , IXenObject obj )
{
try
{
pool . RemoveXCPluginSecret ( session , name , obj ) ;
log . DebugFormat ( "Successfully removed secret for {0}/{1}" , name , Helpers . GetName ( obj ) ) ;
}
catch ( Exception exn )
{
log . Error ( string . Format ( "Failed to remove secret for {0}/{1}" , name , obj ) , exn ) ;
}
}
protected override void Dispose ( bool disposing )
{
base . Dispose ( disposing ) ;
BrowserStates . Clear ( ) ;
if ( Browser ! = null )
{
Browser . Dispose ( ) ;
Browser = null ;
}
if ( _tabPage ! = null )
{
_tabPage . Dispose ( ) ;
_tabPage = null ;
}
}
public void DisposeURL ( IXenObject xmo )
{
if ( BrowserStates . ContainsKey ( xmo ) )
{
BrowserStates . Remove ( xmo ) ;
}
}
public void LaunchHelp ( )
{
if ( HasHelp )
{
Program . OpenURL ( HelpLink ) ;
}
}
private class BrowserState
{
/// <summary>
/// May be null, if this is the XenCenter node.
/// </summary>
public readonly ScriptingObject ObjectForScripting ;
2018-08-06 17:48:39 +02:00
public List < Uri > Uris ;
2013-06-24 13:41:48 +02:00
/// <summary>
/// May be null, if this is the XenCenter node.
/// </summary>
public IXenObject Obj ;
/// <summary>
/// May be null.
/// </summary>
public BrowserCredentials Credentials = null ;
/// <summary>
/// Indicates that the browser is currently in an error state i.e. the page couldn't be loaded. This is used when the console is being replaced
/// by this tab-page so that the user can get the real console back.
/// </summary>
2018-08-06 17:48:39 +02:00
public bool IsError ;
2013-06-24 13:41:48 +02:00
2018-08-06 17:48:39 +02:00
public BrowserState ( List < Uri > uris , IXenObject obj , WebBrowser2 browser )
2013-06-24 13:41:48 +02:00
{
2018-08-06 17:48:39 +02:00
Uris = new List < Uri > ( uris ) ;
2013-06-24 13:41:48 +02:00
Obj = obj ;
ObjectForScripting = obj = = null ? null : new ScriptingObject ( browser , obj ) ;
}
public class BrowserCredentials
{
/// <summary>
/// True if the other fields here are valid (i.e. the user pressed OK, not Cancel, when prompted for credentials).
/// </summary>
internal bool Valid ;
internal string Username ;
internal string Password ;
internal bool PersistCredentials ;
}
}
}
[ComVisible(true)]
public class ScriptingObject
{
private static readonly log4net . ILog log = log4net . LogManager . GetLogger ( System . Reflection . MethodBase . GetCurrentMethod ( ) . DeclaringType ) ;
public const string JAVASCRIPT_CALLBACK_METHOD = "XenCenterXML" ;
2018-02-12 12:54:27 +01:00
[Obsolete("Use SessionOpaqueRef instead.")]
2013-06-24 13:41:48 +02:00
public string SessionUuid ;
2018-02-12 12:54:27 +01:00
public string SessionOpaqueRef ;
2013-06-24 13:41:48 +02:00
public string SessionUrl ;
private IXenConnection connection ;
public WebBrowser2 browser ;
public string SelectedObjectType ;
public string SelectedObjectRef ;
public ScriptingObject ( WebBrowser2 Browser , IXenObject XenObject )
{
browser = Browser ;
connection = XenObject . Connection ;
if ( connection ! = null )
{
2018-08-06 17:48:39 +02:00
connection . ConnectionResult + = connection_ConnectionResult ;
2013-06-24 13:41:48 +02:00
}
SetObject ( XenObject ) ;
}
public void SetObject ( IXenObject XenObject )
{
// Annoyingly all of our classes start with an uppercase character, whereas the servers only uppercase abbreviations
2018-08-06 17:48:39 +02:00
var abbreviations = new List < string > { "SR" , "VDI" , "VBD" , "VM" , "PIF" , "VIF" , "PBD" } ;
2013-06-24 13:41:48 +02:00
SelectedObjectType = XenObject . GetType ( ) . Name ;
2018-08-06 17:48:39 +02:00
if ( abbreviations . Find ( s = > SelectedObjectType . StartsWith ( s ) ) = = null )
2013-06-24 13:41:48 +02:00
{
string firstLetter = SelectedObjectType . Substring ( 0 , 1 ) ;
SelectedObjectType = firstLetter . ToLowerInvariant ( ) + SelectedObjectType . Substring ( 1 , SelectedObjectType . Length - 1 ) ;
}
SelectedObjectRef = XenObject . opaque_ref ;
}
2018-08-02 16:52:13 +02:00
private void connection_ConnectionResult ( object sender , ConnectionResultEventArgs e )
2013-06-24 13:41:48 +02:00
{
if ( connection . IsConnected )
{
// If the connection has been reconnected then we need to update the session ref, as the old one is invalid
LoginSession ( ) ;
Program . Invoke ( Program . MainWindow , delegate
{
2018-08-06 17:48:39 +02:00
if ( ! browser . IsDisposed & & browser . Document ! = null )
2013-06-24 13:41:48 +02:00
browser . Document . InvokeScript ( "RefreshPage" ) ;
else
log . Debug ( "Tried to access disposed webbrowser, ignoring refresh." ) ;
} ) ;
}
}
public void LoginSession ( )
{
if ( connection ! = null )
{
SessionUrl = connection . Session . Url ;
2018-02-12 12:54:27 +01:00
SessionOpaqueRef = connection . DuplicateSession ( ) . opaque_ref ;
#pragma warning disable 612 , 618
SessionUuid = SessionOpaqueRef ;
#pragma warning restore 612 , 618
2013-06-24 13:41:48 +02:00
}
}
/// <summary>
/// This is the method called by any javascript logic in the webpage to make an api call.
/// We take the data and post it straight through to xapi on a background thread to allow
/// the calling method in the JS to exit and the page to continue drawing.
/// </summary>
/// <param name="jsCallback">Method name we will call in the JS to return the server response</param>
/// <param name="data"></param>
public void WriteXML ( string jsCallback , string data )
{
Thread t = new Thread ( writeXML ) ;
t . IsBackground = true ;
t . Start ( new string [ ] { jsCallback , data } ) ;
}
private void writeXML ( object context )
{
try
{
// We post to the json url so that xapi returns the information in a way that is easy to consume by the JS
string [ ] jsCallbackAndData = context as string [ ] ;
HttpWebRequest request = ( HttpWebRequest ) WebRequest . Create ( string . Format ( "{0}/json" , SessionUrl ) ) ;
request . Method = "POST" ;
request . ContentType = "xml" ;
request . ContentLength = Encoding . UTF8 . GetBytes ( jsCallbackAndData [ 1 ] ) . Length ;
2016-01-15 02:57:36 +01:00
request . UserAgent = Branding . BRAND_CONSOLE + "\\Plugin" ;
2016-06-28 14:40:04 +02:00
request . Proxy = XenAdminConfigManager . Provider . GetProxyFromSettings ( connection , true ) ;
2013-06-24 13:41:48 +02:00
using ( StreamWriter xmlstream = new StreamWriter ( request . GetRequestStream ( ) ) )
{
xmlstream . Write ( jsCallbackAndData [ 1 ] ) ;
}
WebResponse response = request . GetResponse ( ) ;
StringBuilder outputBuilder = new StringBuilder ( ) ;
using ( StreamReader reader = new StreamReader ( response . GetResponseStream ( ) ) )
{
while ( reader . Peek ( ) ! = - 1 )
{
outputBuilder . Append ( reader . ReadLine ( ) ) ;
}
}
// The xmlrpc bit of jquery expects a function object, which turns up here as a string that you could eval to get the function
// We just want the name, so we filter it out.
Regex functionNameReg = new Regex ( "^function(.*)\\(" ) ;
string funcName = "" ;
Match m = functionNameReg . Match ( jsCallbackAndData [ 0 ] ) ;
if ( m . Success )
{
funcName = m . Groups [ 1 ] . Value . Trim ( ) ;
}
Program . Invoke ( Program . MainWindow , delegate
{
if ( browser . IsDisposed | | browser . Disposing )
{
2018-08-06 17:48:39 +02:00
log . DebugFormat ( "Browser has been disposed, cannot return message to plugin: {0}" , outputBuilder ) ;
2013-06-24 13:41:48 +02:00
}
else if ( browser . ObjectForScripting ! = this )
{
// If you don't do this, you can get old data re-entering the javascript execution after you have switched to a new object
2018-08-06 17:48:39 +02:00
log . DebugFormat ( "Scripting object has been changed, discarding message to plugin: {0}" , outputBuilder ) ;
2013-06-24 13:41:48 +02:00
}
2018-08-06 17:48:39 +02:00
else if ( browser . Document ! = null )
2013-06-24 13:41:48 +02:00
{
2018-08-06 17:48:39 +02:00
browser . Document . InvokeScript ( JAVASCRIPT_CALLBACK_METHOD , new object [ ] { funcName , outputBuilder . ToString ( ) } ) ;
2013-06-24 13:41:48 +02:00
}
} ) ;
}
catch ( Exception e )
{
log . Error ( e ) ;
}
2018-08-06 17:48:39 +02:00
//TODO: What's the sensible way to let the JS know that there has been an error? Invoke a method with the info? How does this work with regards to message callback?
2013-06-24 13:41:48 +02:00
}
}
}