Both the Wahoo and the WahooControlLibrary assemblies are designed to work within the restricted set of Internet permissions; this meant that it was difficult to implement the complete set of functionality I wanted. I encountered the following challenges:
Gathering Assembly InformationOne nice feature that comes with VS05 is the About Box form wizard, which generates a nice About box form for your Windows Forms project. I used this wizard to create a pretty About box for Wahoo. This ran fine under full permission, of course, but it ran into trouble when executing the following code while running under partial trust: // AboutBox.cs partial class AboutBox : Form { public AboutBox() { ... this.labelVersion.Text = String.Format("Version {0}", AssemblyVersion); } ... public string AssemblyVersion { get { // Will throw a security exception under partial trust // (Internet Zone) return Assembly.GetExecutingAssembly().GetName().Version.ToString(); } } ... } Internally, Assembly.GetName needs access to the file system to determine the assembly version, and this requires FileIOPermission. By default, the Internet zone does not provide FileIOPermission to applications, that explains the security exception that this code raises. Fortunately, the workaround is relatively simple, requiring only that you find an alternative .NET Framework implementation that doesn't need FileIOPermission: the Application.ProductVersion property: // AboutBox.cs partial class AboutBox : Form { ... public string AssemblyVersion { get { // Will run under partial trust (Internet Zone) return Application.ProductVersion.ToString(); } } ... } Application.ProductVersion has no permission requirements and executes without exception in any zone. Handling KeystrokesSafely discovering an assembly's product version is straightforward, but handling keystrokes may be less so. If the user input goes to one of the standard Windows Forms controls, that's not a problem from partially trusted code. However, if a control needs to handle special keysWahooControlLibrary, for example, needs to handle arrow keysthen it must take special measures. Arrow keys, Tab, and Shift+Tab are special keys because of their use in moving between controls in a form. This means that a rogue assembly that is allowed to consume the arrow keys could easily hijack an entire form. For that reason, a control is not allowed to override ProcessDialogKey or IsInputKey, either of which would allow such an activity. The .NET runtime throws a security exception whenever it attempts to compile a method that contains code that creates an instance of a type that overrides these or similar methods, protecting users from a form-jacking. Unfortunately, this means that you can't use these methods to have WahooControlLibrary handle the arrow keys. Another way to handle the arrow keys is to let the parent form retrieve the keys in its own implementation of OnKeyDown (an action that's allowed) and pass them to the control for processing. For a form to handle keystrokes, such as the arrow keys, that can be handled by a child control, the form can set its own KeyPreview property to true. For Wahoo, all this worked fine until experimentation showed that some of the current Windows Forms controls, such as MenuStrip and Button, don't actually let the parent form access these special keys when other controls that allow special keys, such as TextBox, aren't on the form. Because the main Wahoo form contains only a custom control, a MenuStrip, and a StatusStrip, this becomes an issue. As a workaround, the main Wahoo form creates an invisible TextBox and adds it to the list of controls that the form hosts: // MainForm.cs partial class MainForm : Form { ... public MainForm() { ... // HACK: Add a text box so that we can get the arrow keys Controls.Add(new TextBox()); } ... } I'm not proud of this technique, but it lets the arrow keys through in a partially trusted environment, and one does what one must to work around issues in the platform. Communicating via Web ServicesCommunicating with the user is not the only job of a ClickOnce application; often, it must also communicate with the outside world. In the partially trusted zones, this communication is limited to talking back only to the originating site and only via web services, as long as you have checked the "Grant the application access to its site of origin" check box located on the Advanced Security Settings dialog (Project Property Pages | Security | Advanced). Luckily, the originating site is often what we want to talk to anyway, and web services are flexible enough to handle most of our communication needs. Generating the client-side proxy code necessary to talk to a web service is as easy as adding a web reference to your project. You do this by pointing VS05 at the URL for the web service's WSDL (as discussed in Chapter 18: Multithreaded User Interfaces). After the web reference is added, the web service is exposed as a component and is hosted on the Toolbox, enabling you to drag and drop it onto your form and code against it: // MainForm.Designer.cs partial class MainForm { ... private void InitializeComponent() { this.scoresService = new WahooScoresService(); ... // scoresService this.scoresService.Url = "http://localhost/WahooScores/WahooScores.asmx"; ... } ... WahooScoresService scoresService; } // MainForm.cs partial class MainForm : Form { ... void GetHighScores() { ... // Get high scores scores = this.scoresService.GetScores(); // Show high scores ... } void SendHighScore(int score) { // Send high score this.scoresService.RegisterScore(dlg.PlayerName, score); ... } ... } Because partially trusted code is only allowed to make web service calls back to the originating server, it's up to you to make sure that the web service URL points to the originating server. You can do this by replacing the URL that's hard-coded into the web service component (often pointing at http://localhost/...) with the site that you discover dynamically using the ClickOnce deployment framework. First, you acquire the site from which your application was launched. This information is available from the UpdateLocation property, which is exposed by the deployment framework's ApplicationDeployment class: using System.Deployment.Application; ... Uri serverUri = ApplicationDeployment.CurrentDeployment.UpdateLocation; Because update location is dependent on the application having been launched from ClickOnce (rather than by a double-click on application .exe), UpdateLocation has a value only when the application is opened using a deployment manifest (when opened from the Start menu or publish.htm). This means that we need to wrap the property inspection with some ClickOnce detection: // UrlJiggler.cs using System.Deployment.Application; ... public static class UrlJiggler { public static Uri UpdateLocation { get { // If launched via ClickOnce, return the update location if( ApplicationDeployment.IsNetworkDeployed ) { return ApplicationDeployment.CurrentDeployment.UpdateLocation; } return null; } } } After we get the update location URL, we extract the web site from it and jiggle the web service component's URL to redirect it to point to the web service located on the same site as the web site: // UrlJiggler.cs using System.Deployment.Application; using System.Security.Policy; ... public static class UrlJiggler { ... public static Uri UpdateLocation { get {...} } public static string JiggleUrl(string url) { // Get update location Uri updateLocation = UpdateLocation; // Bail if not launched via ClickOnce if( updateLocation == null ) return url; // Extract the site from the update location string site = Site.CreateFromUrl(updateLocation.AbsoluteUri).Name; // Jiggle URL UriBuilder jiggledUrl = new UriBuilder(url); jiggledUrl.Host = site; return jiggledUrl.ToString(); } } Now the client simply calls JiggleUrl: // MainForm.cs public partial class MainForm : Form { public MainForm() { InitializeComponent(); ... // Redirect web service component URL to base site // from which application is being launched this.scoresService.Url = UrlJiggler.JiggleUrl(this.scoresService.Url); } ... } This code enables an application to dynamically adapt to the originating site in Debug mode, when run from VS05, and in Release mode, when run from either an intranet or the Internet. Fundamentally, this code also relies on the deployment framework, which is located in the System.Deployment.Application namespace, as you saw. Although it is beyond the scope of this book, you should know that the deployment framework gives you a fair degree of manual control over the deployment and versioning of an application, particularly when your versioning policies are more complex than those provided by the default ClickOnce configurations available from VS05. [31]
Reading and Writing FilesAfter I got the current high scores via the web service, I found that I wanted to cache them for later access (to savor the brief moment when I was at the top). .NET makes it easy to read and write files and to show the File Save and File Open dialogs. Unfortunately, only a limited subset of that functionality is available in partial trust. In Table 19.1, notice that the Local Intranet zone has unrestricted file dialog permissions but no file I/O permissions. This means that files can be read and written, but not without user interaction. Unrestricted access to the file system is, of course, a security hole on par with buffer overflows and fake password dialogs. To avoid this problem but still allow an application to read and write files, a file can be opened only via the File Save or File Open dialog. Instead of using these dialogs to obtain a file name from the user, we use the dialogs themselves to open the file: // MainForm.cs partial class MainForm : Form { ... void GetHighScores() { ... SaveFileDialog dlg = new SaveFileDialog(); dlg.DefaultExt = ".txt"; dlg.Filter = "Text Files (*.txt)|*.txt|All files (*.*)|*.*"; if( dlg.ShowDialog() == DialogResult.OK ) { // NOTE: Not allowed to get dlg.FileName using( Stream stream = dlg.OpenFile() ) using( StreamWriter writer = new StreamWriter(stream) ) { writer.Write(sb.ToString()); } } ... } ... } Instead of opening a stream using the SaveFileDialog.FileName property after the user has chosen a file, we call the OpenFile method directly. This lets partially trusted code read from a file, but only with user intervention and provided that the code has no knowledge of the file system. Handling Multiple Partial Trust Deployment ZonesIn the preceding example, using the SaveFileDialog's helper methods is fine when you aren't executing in the Internet partial trust zone, which, by default, allows use only of OpenFileDialog. This code causes a security exception to be thrown when it attempts to execute. Rather than remove this code altogetherand the ability to save files along with ityou can refactor the code to selectively execute this code only when the application has the required permissions, which you can detect via the following helper: using System.Security; ... // Check permission static bool HavePermission(IPermission perm) { try { perm.Demand(); } catch( SecurityException ) { return false; } return true; } You can use this method from code: // MainForm.cs partial class MainForm : Form { ... // Check permission static bool HavePermission(IPermission perm) {...} ... void GetHighScores() { ... // Check for permissions to do this // (By default, won't have this permission in the Internet Zone) if( !HavePermission( new FileDialogPermission(FileDialogPermissionAccess.Save)) ) { string s = "This application does not have permission to save files ..."; MessageBox.Show(s, "Wahoo!"); return; } SaveFileDialog dlg = new SaveFileDialog(); dlg.DefaultExt = ".txt"; dlg.Filter = "Text Files (*.txt)|*.txt|All files (*.*)|*.*"; if( dlg.ShowDialog() == DialogResult.OK ) { // NOTE: Not allowed to call dlg.FileName using( Stream stream = dlg.OpenFile() ) using( StreamWriter writer = new StreamWriter(stream) ) { writer.Write(sb.ToString()); } } ... } ... } Debugging Partially Trusted ApplicationsWhichever permission configuration you end up using, you can test how your code executes within the specified permission as you debug your application in VS05. You open the Advanced Security Settings dialog, shown in Figure 19.54, by clicking the Advanced button on the Security tab. Figure 19.54. Configuring Partial Trust Debugging
Additionally, you can grant your application access to the site it was deployed from, as well as specify a real-world URL to simulate any URL-dependent functionality. When you find code that requires permissions that exceed those specified, exceptions are raised and the debugger breaks on the defective code. Consequently, you can either increase the permission requirements for your application in the hope that users will elevate, or you can update the code to cope, using techniques you've just seen. |