NetRun Implementation

NetRun is a Windows application, even though it has no UI of its own. This is important because this way it doesn't require a console window to be open in order to run.

Main Module

We use a number of namespaces to provide easy access to the .NET Framework classes, as shown here:

  using System; using System.Windows.Forms; using System.IO; using System.Reflection; using System.Security; using System.Security.Policy; using System.Security.Permissions;  

Helper Methods

Before we get into the core code, we need some helper functions to parse the URL provided on the command line. We'll need to isolate the application name and the directory for reasons that we'll see shortly. As shown in Figure A-4, the URL provided on the command line will always include both of these.

image from book
Figure A-4: URL for a .NET application

The GetAppDirectory() method will parse the URL and return only the directory, while GetAppName() will return the application name. Add the following code to the AppMain class:

  #region URL parsing functions     public static string GetAppDirectory(string appURL)     {       // get the path without prog name       Uri appURI = new Uri(appURL);       string appPath = appURI.GetLeftPart(UriPartial.Path);       for(int pos = appPath.Length - 1; pos > 0; pos--)         if(appPath.Substring(pos, 1) == "/"               appPath.Substring(pos, 1) == @"\")           return appPath.Substring(0, pos);       return string.Empty;     }     public static string GetAppName(string appURL)     {       // get the prog name without path       Uri appURI = new Uri(appURL);       string appPath = appURI.GetLeftPart(UriPartial.Path);       for(int pos = appPath.Length - 1; pos > 0; pos--)         if(appPath.Substring(pos, 1) == "/"               appPath.Substring(pos, 1) == @"\")           return appPath.Substring(pos + 1);       return string.Empty;     }     #endregion  

Note that these methods are scoped as public , so they're available to all the code in our NetRun project. We'll need these helper methods not only in the AppMain class, but also in the Launcher class.

We'll also need to retrieve the directory path of the current application domain that is, the one in which NetRun itself is running. We can get the full path of NetRun.exe from the .NET runtime, and then we can simply parse out the directory portion of the path, as follows :

  #region GetCurrentDomainPath     private static string CurrentDomainPath()     {       // get path of current assembly       string currentPath =         Assembly.GetExecutingAssembly().CodeBase;       // convert it to a URI for ease of use       Uri currentURI = new Uri(currentPath);   // get the path portion of the URI       string currentLocalPath = currentURI.LocalPath;       // return the full name of the path       return new DirectoryInfo(currentLocalPath).Parent.FullName;     }     #endregion  

We'll use these helper functions as we implement the remainder of the code.

Main Method

The Main() method itself simply provides some top-level exception handling; the actual process of launching the application is handled by another method named RunApplication() as shown here:

  [STAThread]     static void Main(string[] args)     {       if(args.Length != 1)       {         MessageBox.Show("URL to application must be provided",           "Error launching application",           MessageBoxButtons.OK, MessageBoxIcon.Warning);         return;       }       try       {         // launch the app based on the URL provided by the user         RunApplication(args[0]);       }       catch(Exception ex)       {         System.Text.StringBuilder sb =           new System.Text.StringBuilder();         sb.AppendFormat(           "NetRun was unable to launch the application\n{0}\n\n{1}",           args[0], ex.ToString());         MessageBox.Show(sb.ToString(),           "Error launching application",           MessageBoxButtons.OK, MessageBoxIcon.Warning);       }     }  

RunApplication

It's in RunApplication() that we set up and create the new application domain in which our application will run. This is implemented as follows:

  #region RunApplication     private static void RunApplication(string appURL)     {       // create Setup object for the new app domain       AppDomainSetup setupDomain = new AppDomainSetup();       // give it a valid base path       setupDomain.ApplicationBase = CurrentDomainPath();       // give it a safe config file name       setupDomain.ConfigurationFile = appURL + ".remoteconfig";       // create new application domain       AppDomain newDomain = AppDomain.CreateDomain(         GetAppName(appURL), null, setupDomain);       // create Launcher object in new AppDomain       Launcher launcher =         (Launcher)newDomain.CreateInstanceAndUnwrap(         "NetRun", "NetRun.Launcher");       // use Launcher object from the new domain       // to launch the remote app in that AppDomain       launcher.RunApp(appURL);     }     #endregion  

The first step is to create the AppDomainSetup object, which will be used to configure the new application domain. The key element here is that we specify the name of the application configuration file. To do this, we get the path of the EXE (which is our URL), and we append a .remoteconfig extension to it, as shown here:

 setupDomain.ConfigurationFile = appURL + ".remoteconfig"; 

By default, .NET uses the path of the EXE with a .config extension, so all we've really done here is to change it to use a .remoteconfig extension. This way, the download of our configuration file won't be blocked by ASP.NET security.

We then use this AppDomainSetup object to create our new application domain, as follows:

 AppDomain newDomain = AppDomain.CreateDomain(         GetAppName(appURL), null, setupDomain); 

We set the name of the application domain to the name of the application we're dynamically launching. Typically, an application domain's name is the same as that of the EXE that's being run, so this is consistent with default .NET behavior.

To the same method call, we provide a reference to our AppDomainSetup object, so this new domain will be configured using its settings. This means that the .NET Framework code in System.Configuration will read configuration data from the configuration file path we provided.

The next step is to create an instance of the Launcher class in the new application domain . When an application domain is first created, it contains no application code. Until we load application code into the application domain, it has nothing to do. We create a Launcher object in the empty application domain like this:

 Launcher launcher =         (Launcher)newDomain.CreateInstanceAndUnwrap(         "NetRun", "NetRun.Launcher"); 

This code tells the empty application domain to load the NetRun.Launcher class from the NetRun assembly, and then to create an instance of the Launcher class. The result is that our code in the NetRun application domain has a reference to the Launcher class in the new application domain.

Tip 

Technically, our NetRun code has a remoting proxy object that links to the Launcher object in the new application domain. It's not possible to communicate between application domains directly, so remoting is used. In this case, remoting is automatically configured for us by the .NET runtime.

Our last step is to use this reference to call the RunApp() method on the Launcher object, as follows:

 launcher.RunApp(appURL); 

The RunApp() method does the actual work of setting up the serialization work- around, setting security, dynamically loading the EXE, and launching the application. When the application terminates, the RunApp() method makes sure that the FullTrust security is revoked .

Launcher Class

As you've probably begun to understand, most of the hard work happens in the Launcher class. To start with, though, it's very ordinary. The Launcher class uses a number of .NET namespaces to make our coding easier, as follows:

  using System; using System.Reflection; using System.Security; using System.Security.Policy; using System.Security.Permissions;  

The class itself is set up as an anchored class by inheriting from MarshalByRefObject , as shown here:

  public class Launcher : MarshalByRefObject  

We also declare some variables for use by our code, as follows:

  string _appURL;     string _appDir;     string _appName;     bool _groupExisted;  

The first three of these are used to store the full URL, the directory, and the name of the application to be launched. To generate the latter two values, we'll use the helper methods we created in the AppMain class.

The _groupExisted variable is a flag that we'll use to indicate whether the workstation already had security set up for the application's URL. If the workstation does already have security set up for the application's URL, we won't tamper with it. Otherwise, we'll set it up with FullTrust security, and revoke that security when the application terminates.

RunApp

The RunApp() method does the following:

  • Invokes the work-around for the serialization bug

  • Gets and parses the URL for the remote application

  • Sets security for the URL to FullTrust

  • Launches the application

  • In case of an error, displays a message box for the user

  • Revokes the FullTrust security setting

All of the actual work will be done in helper methods, so RunApp() just choreographs the process, as follows:

  public void RunApp(string appURL)     {       // before we do anything, invoke the work-around       // for the serialization bug       SerializationWorkaround();       try       {         // get and parse the URL for the app we are         // launching         _appURL = appURL;         _appDir = AppMain.GetAppDirectory(_appURL);         _appName = AppMain.GetAppName(_appURL);   // TODO: MAKE SURE TO TIGHTEN SECURITY BEFORE USING!!!!         // see http://www.lhotka.net/Articles.aspx?         // id=2f5a8115-b425-4aa1-bae2-b8f80766ecb3         SetSecurity();         // load the assembly into our AppDomain         Assembly asm = Assembly.LoadFrom(appURL);         // run the program by invoking its entry point         asm.EntryPoint.Invoke(asm.EntryPoint, null);       }       finally       {         RemoveSecurity();       }     }  

The core of the method is where we dynamically load and launch the application. To do this, we ask the .NET runtime to load the EXE from the URL, as follows:

 Assembly asm = Assembly.LoadFrom(appURL); 

This seemingly trivial bit of code triggers the no-touch deployment process. The .NET runtime automatically detects that the path to the EXE is a URL, and downloads the EXE into a user-specific cache on the client workstation.

If the EXE was already in the cache, the .NET runtime does a file date/time check to compare the cached version against the version on the remote server. If they match, the file isn't downloaded, and the cached version is used. Otherwise, the new version is downloaded into the cache.

As the code in the EXE runs, it might call code in external DLLs. When that happens, the appropriate DLL is downloaded into the cache. Again, the file date/time comparison is used, so these DLLs are only downloaded if needed.

Note that this means that only DLLs that are actually used are downloaded. Our EXE could have references to DLLs containing code that's not calledand in that case, those DLLs are never downloaded to the client.

Similarly, if our code uses an application configuration file, that too is automatically downloaded. The name of the configuration file was specified when the application domain was created, and we made sure that it's looking for a file with a .remoteconfig extension so ASP.NET security doesn't block the download.

Once the EXE has been loaded into memory within the application domain, we invoke its entry point, as follows:

 asm.EntryPoint.Invoke(asm.EntryPoint, null); 

This actually runs the application. All .NET applications (EXEs) have a well-known method called an entry point . The compiler creates this when the EXE is created, and the .NET runtime uses it to launch the application. By invoking the entry-point method, we're simply emulating what the .NET runtime would normally do.

The call to Invoke() is synchronous, which means that it won't return from this call until the client application has terminated . This means that the next line of code, which revokes FullTrust security from the URL, won't run until after the application has finished.

Serialization Bug Work-around

The serialization bug (or more accurately, the de serialization bug) occurs because the .NET runtime fails to find our assembly in memory. Our assembly actually is in memory, but .NET fails to find it because .NET keeps a couple of lists of the assemblies that are currently loadedand the dynamically loaded assemblies are in their own list. For some reason, the deserialization process doesn't check that particular list to find assemblies.

Fortunately, when it fails to find an assembly, the runtime raises an AssemblyResolve event from the AppDomain object. We can handle this event and provide the runtime with a reference to the right assembly.

Providing the reference isn't hardthe assembly is already loaded, and the AppDomain object has a list of all the currently loaded assemblies, so all we need to do is scan through that list of assemblies to find the right one.

The code to implement this comes in two parts . First, there's the SerializationWorkaround() method, which just adds a handler for the AssemblyResolve event:

  #region Serialization bug workaround     private void SerializationWorkaround()     {       // hook up the AssemblyResolve       // event so deep serialization works properly       // this is a work-around for a bug in the .NET runtime       AppDomain.CurrentDomain.AssemblyResolve +=         new System.ResolveEventHandler(ResolveEventHandler);     }     #endregion  

Then there's the event handler itself. This method gets a parameter of type ResolveEventArgs , which includes the name of the assembly that the .NET runtime failed to find. All we need to do is get a list of all the assemblies that are currently loaded, and then scan through them to find the one with the matching name, as shown here:

  private Assembly ResolveEventHandler(       object sender, ResolveEventArgs e)     {       // get a list of all the assemblies loaded in our AppDomain       Assembly [] list = AppDomain.CurrentDomain.GetAssemblies();   // search the list to find the assembly       // that was not found automatically       // and return the assembly from the list       foreach(Assembly asm in list)         if(asm.FullName == e.Name)           return asm;       // we didn't find it either, so return null       return null;     }  

If we find the right assembly, we simply return it as a result, and the .NET runtime will use it to deserialize our object, solving the problem. If we don't find the assembly, we return null , and the .NET runtime will continue as normal. This means that it will throw an exception indicating that the assembly couldn't be found, and our application will terminate.

Set FullTrust Security

The .NET runtime applies code-based security to all the code we run. As we've discussed, code is grouped into zones based on where it came from. Code that comes from our hard drive is fully trusted, code that comes from an intranet location or network drive is more restricted, and code from other locations (such as the Internet) is restricted further still.

Note 

Altering security settings always comes with some risk. If we alter security for the Internet zone, or even for a single Internet URL, we can allow malicious code to run on our machine with enough permission to do harm.

This implementation of NetRun grants FullTrust securitythe highest levelto the URL from where our application is being downloaded. This means that any application run via this version of NetRun can do pretty much anything it likes on the client machinejust like a regular Windows application or COM component has been able to do for the past decade or more.

In a production environment, you may want to alter NetRun so that it only grants FullTrust permissions to URLs that are within the LocalIntranet_Zone , or where the domain name matches the domain of your organization. This would prevent users from running arbitrary Internet code with FullTrust permissions.

We grant FullTrust permissions to a URL by using some of the security classes built into the .NET Framework. Permissions in .NET are grouped under Enterprise , Machine , and User areas. Within each of these are code groups that define the categories or zones for permissions.

This is most easily displayed by using the .NET Framework Configuration tool as shown in Figure A-5.

image from book
Figure A-5: .NET Framework Configuration tool

In Figure A-5, we can see the zones defined at the Machine level for the current computer. Each of these zones has a set of permissions. The My_Computer_Zone , for instance, has FullTrust , while the Restricted_Zone has no permissions at allthat is, no code from that zone will execute.

To grant extra permissions to a URL, we need to add a new code group to this list, and associate that code group with a set of permissionsin our case, the FullTrust permission set. That's what SetSecurity() does, as shown here:

  #region SetSecurity to FullTrust     private void SetSecurity()     {       System.Collections.IEnumerator ph = null;       System.Security.Policy.PolicyLevel pl = null;       bool found = false;       // retrieve the security policy hierarchy       ph = SecurityManager.PolicyHierarchy();   // loop through to find the Machine level subtree       while(ph.MoveNext())       {         pl = (PolicyLevel)ph.Current;         if(pl.Label == "Machine")         {           found = true;           break;         }       }       if(found)       {         // see if the CodeGroup for this app already exists         // as a Machine-level entry         foreach(CodeGroup cg in pl.RootCodeGroup.Children)         {           if(cg.Name == _appName)           {             // CodeGroup already exists             // we assume it is set to a valid             // permission level             _groupExisted = true;             return;           }         }         // the CodeGroup doesn't already exist, so         // we'll add a URL group with FullTrust         _groupExisted = false;         UnionCodeGroup ucg =           new UnionCodeGroup(           new UrlMembershipCondition(_appDir + "/*"),           new PolicyStatement(           new NamedPermissionSet("FullTrust")));         ucg.Description = "Temporary entry for " + _appURL;         ucg.Name = _appName;         pl.RootCodeGroup.AddChild(ucg);         SecurityManager.SavePolicy();       }     }     #endregion  

The first step is to retrieve the security-policy tree, or hierarchy, and then scan through it to find the Machine node (as shown in Figure A-5):

 // retrieve the security PolicyHierarchy     ph = SecurityManager.PolicyHierarchy();     // loop through to find the Machine-level subtree     while(ph.MoveNext())     {       pl = (PolicyLevel)ph.Current;       if(pl.Label == "Machine")       {         found = true;         break;       }     } 

Once we've found the Machine node, we can scan the list of child nodes to see if there's already a code group with the same name as our application, as follows:

 foreach(CodeGroup cg in pl.RootCodeGroup.Children)         {           if(cg.Name == _appName)           {             // CodeGroup already exists             // we assume it is set to a valid             // permission level             _groupExisted = true;             return;           }         } 

Using the application name as the code group name is somewhat arbitrary, but it means that the node name is meaningful in case someone should use the .NET configuration management while our application is running. It also means that we can manually add a node using that tool, in case we want to assign different permissions for a specific application manually. Notice that if our code detects a preexisting code group, we simply exit and don't modify or update its existing set of permissions.

If no code group exists for our application, then we need to add one. This is done by creating a new code group object that specifies both the URL where our code resides, and the FullTrust permission set we want to associate with the group, as shown here:

 UnionCodeGroup ucg =           new UnionCodeGroup(           new UrlMembershipCondition(_appDir + "/*"),           new PolicyStatement(           new NamedPermissionSet("FullTrust"))); 

We then set the new code group's description and name to meaningful values, as follows:

 ucg.Description = "Temporary entry for " + _appURL;         ucg.Name = _appName; 

Finally, we add the new code group to the security-policy tree and then save the policy tree, thereby making the change effective, as shown here:

 pl.RootCodeGroup.AddChild(ucg);         SecurityManager.SavePolicy(); 

At this point, any code (EXEs or DLLs) that we run from this URL will do so with FullTrust permissions, just as those assemblies would if they'd been manually installed on our machine's hard drive.

Revoke FullTrust Security

Removing a code group is pretty straightforward. All we need to do is scan through the security-policy hierarchy to find the Machine node. Then, within that node, we look for the code group we inserted based on the application name. When we find it, we simply remove it.

However, we only do this if we actually inserted the code group. If there was a preexisting code group of this name, we didn't add or alter the code group when we set up security, so we shouldn't remove or alter it here either. The following code takes care of this:

  #region RemoveSecurity     private void RemoveSecurity()     {       // if the group existed before NetRun was used       // we want to leave the group intact, so we       // can just exit       if(_groupExisted) return;       // on the other hand, if the group didn't already       // exist then we need to remove it now that       // the business application is closed       System.Collections.IEnumerator ph = null;       System.Security.Policy.PolicyLevel pl = null;       bool found = false;       // retrieve the security PolicyHierarchy       ph = SecurityManager.PolicyHierarchy();       // loop through to find the Machine-level subtree       while(ph.MoveNext())       {         pl = (PolicyLevel)ph.Current;         if(pl.Label == "Machine")         {           found = true;           break;         }       }   if(found)       {         // see if the CodeGroup for this app exists         // as a Machine-level entry         foreach(CodeGroup cg in pl.RootCodeGroup.Children)         {           if(cg.Name == _appName)           {             // CodeGroup exits - remove it             pl.RootCodeGroup.RemoveChild(cg);             SecurityManager.SavePolicy();             break;           }         }       }     }     #endregion  

First, we check to see if the code group existed in the first place. If it did, we simply exit without doing any work, as shown here:

 if(_groupExisted) return; 

The _groupExisted variable was set in our SetSecurity() method when we searched for the code group.

Assuming it was NetRun that added the code group, we need to find and remove it. First, we scan through the policy hierarchy to find the Machine node, just like we did in SetSecurity() . Then, we scan through the child nodes looking for the node that's named the same as our application, just like we did in SetSecurity() . If we find such a node, then we remove it, as follows:

 if(cg.Name == _appName)         {           // CodeGroup exits - remove it           pl.RootCodeGroup.RemoveChild(cg);           SecurityManager.SavePolicy();           break;         } 

We just remove the node from the policy hierarchy and then save the hierarchy, which applies our changes to the system.

At this point, the application has closed and we've cleaned up after ourselves by removing the temporary code group we added. The system is just as it was before we started.



Expert C# Business Objects
Expert C# 2008 Business Objects
ISBN: 1430210192
EAN: 2147483647
Year: 2006
Pages: 111

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net