VSM Cover Story

Simplify Programmatic File Access

Access files in different locations using the same code, whether the files reside on a file server, a Web server, or an FTP server.

Technology Toolbox: Visual Basic, C#, P/Invoke & Intertop, Win32 SDK

Network architectures often present a moving target to developers. New zones are created, firewalls inserted, and domain relationships revised, usually as a result of management policy decisions regarding risk and related benefit. One of the policy changes that cause developers headaches relates to NetBIOS file access. NetBIOS is a notorious security problem, so a lot of organizations are allowing it only in the most secure network zones. Unfortunately, production applications with dependencies on NetBIOS often restrict their options. If you take away NetBIOS you have to change those applications, and that's expensive. Because of that cost, managers can't always choose the best option in terms of application functionality and security.

When writing code, you should strive to give management options, not take them away. The more changes your code can accommodate, the more valuable it is, and the more time you can spend writing new functionality. The value of your code is directly related to its flexibility.

Sometimes writing flexible code means selecting the right third-party control or the right design pattern. Sometimes it means considering what changes your code is likely to see and design for them. But it doesn't always require writing a complicated framework.

File access is one of these cases. When reading a file in .NET, you probably write code similar to this:

Private Sub ReadFile(ByVal FileName _
As String)
Dim Reader As TextReader

Using Input As Stream = New
FileStream( _
FileName, _
FileMode.Open, FileAccess.Read)
Reader = New StreamReader(Input)
End Using

End Sub

This code is inflexible in the face of network architecture changes. FileStream can only read files using a local path, mapped network drive, or UNC path. If you have to change your access protocol to HTTP or FTP, you also have to change your code.

There is a better way. The .NET Framework features underused functionality that allows you to access files using a local path, UNC path, HTTP URL, or FTP URL, all with the same application code. The technique I'll show you is not perfect, though, so I'll address its shortcomings using a design pattern called Abstract Factory first identified in the classic "Gang of Four" book Design Patterns: Elements of Reusable Object-Oriented Software.

Remember when downloading a file from the Internet was pure torture? .NET's System.Net namespace makes this once difficult task extremely easy:

Dim Request As WebRequest
Dim Response As WebResponse
Dim DataStream As Stream

Request = WebRequest.Create( _
Response = Request.GetResponse
DataStream = Response.GetResponseStream

This code also works for local files. You can substitute "C:\MyFile.txt" for "http://www.ftponline.com/MyFile.txt" and read MyFile.txt from "C:\" using that same code. This magic happens in WebRequest's Create method. Create's return value (as declared) is WebRequest, but it doesn't return a WebRequest. Instead, it returns a subclass of WebRequest specialized for the particular URI scheme you pass it. If you pass it an HTTP URL, you get an instance of HttpWebRequest. If you pass it an FTP URL, you get an instance of FtpWebRequest. If you pass it a FILE URL, you get an instance of FileWebRequest. The lesson here is that you can achieve a good degree of protocol-independence in your applications if you ignore the derived classes and code against the members of the base class, WebRequest (see Figure 1).

Build on Abstract Factory
The solution described in this article relies on the venerable Abstract Factory design pattern. If you are new to design patterns, the Abstract Factory is a great place to start because you've probably seen it before—and you will almost certainly see it again—even if you didn't know it had a name.

The .NET Framework uses the Abstract Factory extensively. XmlReader and XmlWriter also use it, as do the classes in the System.Security.Cryptography namespace. There are almost as many variations on the Abstract Factory design pattern as there are implementations, but the common elements are the abstract base class (WebRequest) called the Abstract Product, the specialized subclasses called Concrete Products, and the application code that consumes Product Objects called the Client.

For your own code, I recommend using the Abstract Factory design pattern in cases where you can stick to the functionality exposed by the Abstract Product. If you find yourself writing client code to treat the various Concrete Products differently, this pattern is probably not an appropriate choice. For example, if you need to use HttpWebRequest's IfModifiedSince property to retrieve a file only if has changed, you probably need to define your own strategy and create multiple implementations of it. Abstract Factory is an object-creation pattern. It can't make two different problems any less different.

So far I've covered the benefits of using the WebRequest factory to access local files, but there are some drawbacks to keep in mind. One drawback is that you give up certain capabilities when using the FileWebRequest class, such as seeking and appending. This drawback is negated substantially by the fact that most of today's business applications can live with these restrictions.

Another drawback is more serious. The base class WebRequest has a property named Credentials that lets you specify a user who has access to the resource. HttpWebRequest and FtpWebRequest support this property, but FileWebRequest does not. The class won't throw an exception if you set this property before calling GetResponse. But if you try to access a restricted resource—a resource that the default credentials don't provide access to—an AccessDeniedException will be thrown even if your own credentials would normally allow access to that resource. Trying to get around this drawback by wetting the UseDefaultCredentials property throws a NotSupportedException with this message: "This property is not supported by this class."

Fortunately, impersonation provides an easy solution. This might sounds like a devilishly complex issue requiring guru-like expertise, but running code under a secondary logon is easy. All you need is a Windows account token. Pass an account token to the WindowsIdentity class' Impersonate method, and you receive back an instance of WindowsImpersonationContext. At this point, you're impersonating the specified user. To exit the impersonation block, call WindowsImpersonationContext.Dispose inside a Using block:

Using WindowsIdentity.Impersonate(AdminToken)

'   All code in this block executes as impersonated

End Using

Acquiring a Windows account token requires logging on as the specified user. The .NET Framework has no API for the purpose, so we must P/Invoke the Windows API function LogonUser. Microsoft's C-centric Platform SDK documentation defines LogonUser as such:

BOOL LogonUser(
   LPTSTR lpszUsername,
   LPTSTR lpszDomain,
   LPTSTR lpszPassword,
   DWORD dwLogonType,
   DWORD dwLogonProvider,
   PHANDLE phToken

Note that there are two functions related to LogonUser, as is the case with most Windows API functions that accept strings. The first, LogonUserA, accepts ANSI strings; the other, LogonUserW, accepts Unicode strings. Accepted practice holds that you should use the Unicode functions wherever possible when using P/Invoke Windows functions. The ANSI functions exist only for legacy compatibility. LogonUserW's first three parameters are self-explanatory. The fourth and fifth determine what type of authentication to use and what the resulting session will be able to do. In this example, you want to specify LOGON32_LOGON_INTERACTIVE and LOGON32_PROVIDER_DEFAULT. The last is an output parameter that contains the handle of the new session if the call is successful:

<DllImport("advapi32.dll", EntryPoint:="LogonUserW", _
   CharSet:=CharSet.Unicode)> _
   Public Shared Function LogonUser _
   ( _
   ByVal UserName As String, _
   ByVal Domain As String, _
   ByVal Password As String, _
   ByVal Type As UInt32, _

   ByVal Provider As UInt32, _
   <Out()> ByRef LogonToken As IntPtr _
   ) _
   As Boolean
End Function

The next step is to assemble everything together:

Dim LogonToken As IntPtr
Dim Request As WebRequest
Dim ResponseStream As Stream
Dim Response As WebResponse

If LogonUser("Administrator", ".", "top_secret", _
   Using WindowsIdentity.Impersonate(LogonToken)
      Request = WebRequest.Create("C:\MyFile.txt")
      Response = Request.GetResponse
      ResponseStream = Response.GetResponseStream
   End Using

'   Read ResponseStream here

   Throw New SecurityException("Logon failed.")
End If

You have a problem here, however. You want to submit your WebRequest only in an impersonated code block if you are accessing a local file and doing so with non-default credentials. This special case violates the Abstract Factory design pattern's all-concrete-products-are-equal rule.

An elegant solution is to wrap .NET's factory in a factory of your own. Call this custom factory StreamFactory. Note that StreamFactory's only public method, Create, is overloaded (see Listing 1). This effectively makes the NetworkCredential instance optional. Both Create methods result in calls to either CreateNormal or CreateSpecial. CreateNormal is "pass-through" functionality; it submits a WebRequest and returns the associated ResponseStream. CreateSpecial is where you inject new functionality such as logging on the specified user, impersonating the user, and performing the request.

Sometimes change-proofing your code does not require an elaborate edifice. Instead, combining the right design pattern with smart use of out-of-the-box functionality can do the trick. All other things being equal, a simpler solution is a better solution.

About the Author

John Cronan is Lead Scientist at Devia Software, LLC.

comments powered by Disqus


Subscribe on YouTube