Just a short, simple blog for Bob to share his thoughts.
27 October 2013 • by Bob • FrontPage, Macros, VBA
A few months ago I wrote a blog titled Using FrontPage 2003 to Bulk Rename Images Using VBA, in which I shared a VBA macro that renamed all of the images in a website to a common file-naming syntax. In that blog I explained my reasoning behind my use of the long-outdated FrontPage 2003, and that reason was that FrontPage's "Link Fix Up" feature replicates file renames across your entire website. This single feature can greatly reduce your development time for websites when you have a lot of renaming to do.
Recently I ran into another interesting situation where combining with FrontPage's VBA and "Link Fix Up" features saved me an incredible amount of time, so I thought that I would share that in today's blog.
I recently inherited a large website with thousands of images that were spread across dozens of folders throughout the website. Unfortunately, this website was created by several developers, so there were a large number of duplicate images scattered throughout the website.
It would have taken me several days to remove all of the duplicates and edit all of the HTML in the web pages, so this seemed like a task that was better suited for automation in FrontPage 2003.
The following VBA macro for FrontPage 2003 will locate every image in a website, and it will move all images to the website's root-level "images" folder if they are not already located in that folder:
Public Sub MoveImagesToImagesFolder() Dim objFolder As WebFolder Dim objWebFile As WebFile Dim intCount As Integer Dim strExt As String Dim strRootUrl As String Dim strImagesUrl As String Dim blnFound As Boolean ' Define the file extensions for image types. Const strValidExt = "jpg|jpeg|gif|bmp|png" ' Define the images folder name. Const strImagesFolder = "images" With Application ' Retrieve the URL of the website's root folder. strRootUrl = LCase(.ActiveWeb.RootFolder.Url) ' Define the root-level images folder URL. strImagesUrl = LCase(strRootUrl & "/" & strImagesFolder) ' Set the initial search status to not found. blnFound = False ' Loop through the root-level folders. For Each objFolder In .ActiveWeb.RootFolder.Folders ' Search for the images folder. If StrComp(objFolder.Url, strImagesUrl, vbTextCompare) = 0 Then ' Exit the loop if the images folder is found. blnFound = True Exit For End If Next ' Test if the images folder is missing... If blnFound = False Then ' ... and create it if necessary. .ActiveWeb.RootFolder.Folders.Add strImagesFolder End If ' Loop through the collection of images. For Each objWebFile In .ActiveWeb.AllFiles ' Retrieve the file extension. strExt = LCase(objWebFile.Extension) ' Test if the file extension is for an image type. If InStr(1, strValidExt, strExt, vbTextCompare) Then ' Test if the image is in the root-level images folder... If StrComp(objWebFile.Parent, strImagesUrl, vbTextCompare) <> 0 Then ' ... and move the file if it is not. objWebFile.Move strImagesUrl & "/" & objWebFile.Name, True, True End If End If Next End With End Sub
This macro is pretty straight-forward, but there are a couple of parameters that I pass to the WebFile.Move()
method which I would like to point out. The first parameter for the Move()
is the destination URL, which should be obvious, but the second and third parameters should be explained:
Another thing to note is that you can easily update this macro to move other file types. For example, you could move all of the JavaScript files in your website to a common root-level "scripts" folder by changing the values of the strValidExt
and strImagesFolder
constants.
As always, have fun... ;-]
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
26 October 2013 • by Bob • Extensibility, FTP, Visual Studio
I've written a lot of walkthroughs and blog posts about creating custom FTP providers over the past several years, and I usually include instructions like the following example for adding a custom post-build event that will automatically register your extensibility provider in the Global Assembly Cache (GAC) on your development computer:
net stop ftpsvc call "%VS100COMNTOOLS%\vsvars32.bat">nul gacutil.exe /if "$(TargetPath)" net start ftpsvc
And I usually include instructions like the following example for determining the assembly information for your extensibility provider:
Over time I have changed the custom post-build event that I use when I am creating custom FTP providers, and my changes make it easier to register custom FTP providers. With that in mind, I thought that my changes would make a good blog subject.
First of all, if you take a look at my How to Use Managed Code (C#) to Create a Simple FTP Authentication Provider walkthrough, you will see that I include instructions like my earlier examples to create a custom post-build event and retrieve the assembly information for your extensibility provider.
That being said, instead of using the custom post-build event in that walkthrough, I have started using the following custom post-build event:
net stop ftpsvc call "$(DevEnvDir)..\Tools\vsvars32.bat" gacutil.exe /uf "$(TargetName)" gacutil.exe /if "$(TargetPath)" gacutil.exe /l "$(TargetName)" net start ftpsvc
This script should resemble the following example when entered into Visual Studio:
This updated script performs the following actions:
Let's say that you created a simple FTP authentication provider which contained code like the following example:
using System; using System.Text; using Microsoft.Web.FtpServer; public class FtpTestProvider : BaseProvider, IFtpAuthenticationProvider { private string _username = "test"; private string _password = "password"; public bool AuthenticateUser( string sessionId, string siteName, string userName, string userPassword, out string canonicalUserName) { canonicalUserName = userName; if (((userName.Equals(_username, StringComparison.OrdinalIgnoreCase)) == true) && userPassword == _password) { return true; } else { return false; } } }
When you compile your provider in Visual Studio, the output window should show the results of the custom post-build event:
When you examine the output information in detail, the highlighted area in the example below should be of particular interest, because it contains the assembly information for your extensibility provider:
------ Rebuild All started: Project: FtpTestProvider, Configuration: Debug Any CPU ------
FtpTestProvider -> c:\users\foobar\documents\visual studio 2012\Projects\FtpTestProvider\bin\Debug\FtpTestProvider.dll
The Microsoft FTP Service service is stopping..
The Microsoft FTP Service service was stopped successfully.
Microsoft (R) .NET Global Assembly Cache Utility. Version 4.0.30319.17929
Copyright (c) Microsoft Corporation. All rights reserved.
Assembly successfully added to the cache
Microsoft (R) .NET Global Assembly Cache Utility. Version 4.0.30319.17929
Copyright (c) Microsoft Corporation. All rights reserved.
The Global Assembly Cache contains the following assemblies:
FtpTestProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=eb763c2ec0efff75, processorArchitecture=MSIL
Number of items = 1
The Microsoft FTP Service service is starting.
The Microsoft FTP Service service was started successfully.
========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========
Once you have that information, you simply need to reformat it as "FtpTestProvider, FtpTestProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=eb763c2ec0efff75
" in order to enter it into the FTP Custom Authentication Providers dialog box in the IIS Manager, or by following the steps in my FTP Walkthroughs or my Adding Custom FTP Providers with the IIS Configuration Editor blogs.
That wraps it up for today's post. As always, let me know if you have any questions. ;-]
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
13 October 2013 • by Bob • Military, Rants
One of my family members posted the following picture to Facebook:
I'm not a teacher so I can't speak about the veracity of that statement, but nevertheless I felt compelled to post the following response:
"Not true - when you're in the military, it's much worse. Here's just one example from my years in the service:
"I had food poisoning and I spent the night throwing up so much that I lost 10 pounds in one day. (Seriously.) But the military owns you, so you can't just call in sick. Despite feeling like I was about to die, I had to drive 30 minutes to the military post and show up for a morning formation, where I stood at-attention or at-ease for a 30-minute summary of the days' news and events. After that formation ended, all of people who wanted to go on sick call were ordered to fall out to a separate formation, where I had to describe my symptoms to the NCOIC, who was tasked with determining if anyone should actually be allowed to head to the clinic or go back to work.
"Bear in mind that the clinic that I was sent to was not a hospital where I would see an actual doctor, but a tiny building where no one had any formal medical education. For that matter, sick call is a horrible experience where you eventually get to meet up with a disgruntled E-4 who's sorry that he volunteered for his MOS and generally wants to take out that frustration on every person who shows up; since he has no formal education, he is only capable of reading symptoms from a book to diagnose you, so it's a miracle that more deaths do not occur in military clinics due to gross negligence. (Although I have long stories about deaths and permanent injuries that were the direct result of misdiagnoses in military clinics, but I'm getting ahead of myself.)
"Before I got to see said disgruntled E-4, I had to sit in a waiting room for around an hour, so by the time I was finally sent to an examining room I had been on post for several hours and it was probably approaching noon. The E-4 was able to figure out that I was seriously ill pretty quickly - all of the vomiting was an easy clue. He decided by taking my blood pressure that I was severely dehydrated, (duh), so I spent the next few hours lying on a cot with IV bags in my arms until he decided that I was sick enough to be put on quarters for the rest of the day and I was sent home.
"By the time that I finally arrived at [my wife's and my] apartment it was sometime in the late afternoon, which is when the normal workday would probably have been over for most civilians. But when you're in the military they try to make your illness so unbearable that you'd rather show up to work, so here's the worst part: when you have something like the flu that lasts more than a day, you have to repeat the process that I just described until your illness has passed or you are admitted to a hospital because you are not recovering. Of course, having to sit in a clinic with a score of other sick people means that everyone is trading illnesses, so you have this great breeding ground of diseases in the military, which undoubtedly helped cause a great deal of the military fatalities during the great influenza outbreak in the early 20th century.
"So being sick as a teacher may be awful, but trust me - you could have it a lot worse."
As a parting thought, there may be some qualified people in Army medicine, but I have always pointed out that people who graduate at the top of their class in medical school do not become Army doctors; they take prestigious positions at world-class hospitals. Who usually winds up as military doctors? Medical students with barely passing grades and large student loans to pay off.
Given a choice between a doctor with a 4.0 GPA from Harvard Medical School or a doctor with a 2.5 GPA from The Podunk Medical Academy for the Emotionally Challenged, who would you pick? Well, when you're in the Army, you don't get to pick. And unless you're a general, it's usually the latter of those two choices.
I have always summarized Army medicine as follows: "You get what you pay for when you see an Army doctor; you pay nothing, and you get nothing."
20 September 2013 • by Bob • IIS, Scripting, WebDAV
A couple of years ago I wrote a blog that was titled "How to create an HTML Application to configure your WebDAV Redirector settings", where I showed how to use HTMLA to create a simple editor for most of the WebDAV Redirector settings. These settings have no other user interface, so prior to my blog post users had to manually edit the registry in order to modify their WebDAV Redirector settings.
![]() |
Click image to expand |
In the past two years since I wrote that blog, I have found myself using that simple application so often that I now keep it in my personal utilities folder on my SkyDrive so I can have it with me no matter where I am travelling. But that being said, I ran into an interesting situation the other day that made me want to update the application, so I thought that it was time to write a new blog with the updated changes.
Here's what happened - I had opened my application for modifying my WebDAV Redirector settings, but then something happened which distracted me, and then I headed off to lunch before I committed my changes to the registry. When I came back to my office, I noticed that my WebDAV Redirector settings application was still open and I clicked the Exit Application button. The application popped up a dialog which informed me that I had changes that hadn't been saved to the registry, but I forgot what they were. This put me in a quandary - I could simply click Yes and hope for the best, or I could click No and lose whatever changes that I had made and re-open the application to start over.
It was at that time that I thought to myself, "If only I had a Reset Values button..."
By now you can probably see where this blog is going, and here's what the new application looks like - it's pretty much the same as the last application, with the additional button that allows you to reset your values without exiting the application. (Note - the application will prompt you for confirmation if you attempt to reset the values and you have unsaved changes.)
![]() |
Click image to expand |
To create this HTML Application, you need to use the same steps as my last blog: save the following HTMLA code as "WebDAV Redirector Settings.hta" to your computer, and then double-click its icon to run the application.
<html> <head> <title>WebDAV Redirector Settings</title> <HTA:APPLICATION APPLICATIONNAME="WebDAV Redirector Settings" ID="WebDAV Redirector Settings" VERSION="1.0" BORDER="dialog" BORDERSTYLE="static" INNERBORDER="no" SYSMENU="no" MAXIMIZEBUTTON="no" MINIMIZEBUTTON="no" SCROLL="no" SCROLLFLAT="yes" SINGLEINSTANCE="yes" CONTEXTMENU="no" SELECTION="no"/> <script language="vbscript"> ' ---------------------------------------- ' Start of main code section. ' ---------------------------------------- Option Explicit Const intDialogWidth = 700 Const intDialogHeight = 620 Const HKEY_LOCAL_MACHINE = &H80000002 Const strWebClientKeyPath = "SYSTEM\CurrentControlSet\Services\WebClient\Parameters" Const strLuaKeyPath = "Software\Microsoft\Windows\CurrentVersion\Policies\System" Dim objRegistry Dim blnHasChanges ' ---------------------------------------- ' Start the application. ' ---------------------------------------- Sub Window_OnLoad On Error Resume Next ' Set up the UI dimensions. Self.resizeTo intDialogWidth,intDialogHeight Self.moveTo (Screen.AvailWidth - intDialogWidth) / 2, _ (Screen.AvailHeight - intDialogHeight) / 2 ' Retrieve the current settings. Document.all.TheBody.ClassName = "hide" Set objRegistry = GetObject( _ "winmgmts:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv") Call CheckForLUA() Call GetValues() Document.All.TheBody.ClassName = "show" End Sub ' ---------------------------------------- ' Check for User Access Control ' ---------------------------------------- Sub CheckForLUA() If GetRegistryDWORD(strLuaKeyPath,"EnableLUA",1)<> 0 Then MsgBox "User Access Control (UAC) is enabled on this computer." & _ vbCrLf & vbCrLf & "UAC must be disabled in order to edit " & _ "the registry and restart the service for the WebDAV Redirector. " & _ "Please disable UAC before running this application again. " & _ "This application will now exit.", _ vbCritical, "User Access Control" Self.close End If End Sub ' ---------------------------------------- ' Exit the application. ' ---------------------------------------- Sub ExitApplication() If blnHasChanges = False Then If MsgBox("Are you sure you want to exit?", _ vbQuestion Or vbYesNo Or vbDefaultButton2, _ "Exit Application") = vbNo Then Exit Sub End If Else Dim intRetVal intRetVal = MsgBox("You have unsaved changes. " & _ "Do you want to save them before you exit?", _ vbQuestion Or vbYesNoCancel Or vbDefaultButton1, _ "Reset Application") If intRetVal = vbYes Then Call SetValues() ElseIf intRetVal = vbCancel Then Exit Sub End If End If Self.close End Sub ' ---------------------------------------- ' Reset the application. ' ---------------------------------------- Sub ResetApplication() If blnHasChanges = True Then Dim intRetVal intRetVal = MsgBox("You have unsaved changes. " & _ "Do you want to save them before you reset the values?", _ vbQuestion Or vbYesNoCancel Or vbDefaultButton1, _ "Reset Application") If intRetVal = vbYes Then Call SetValues() ElseIf intRetVal = vbCancel Then Exit Sub End If End If Call GetValues() End Sub ' ---------------------------------------- ' Flag the application as having changes. ' ---------------------------------------- Sub FlagChanges() blnHasChanges = True End Sub ' ---------------------------------------- ' Retrieve the settings from the registry. ' ---------------------------------------- Sub GetValues() On Error Resume Next Dim tmpCount,tmpArray,tmpString ' Get the radio button values Call SetRadioValue(Document.all.BasicAuthLevel, _ GetRegistryDWORD(strWebClientKeyPath, _ "BasicAuthLevel",1)) Call SetRadioValue(Document.all.SupportLocking, _ GetRegistryDWORD(strWebClientKeyPath, _ "SupportLocking",1)) ' Get the text box values Document.all.InternetServerTimeoutInSec.Value = _ GetRegistryDWORD(strWebClientKeyPath, _ "InternetServerTimeoutInSec",30) Document.all.FileAttributesLimitInBytes.Value = _ GetRegistryDWORD(strWebClientKeyPath, _ "FileAttributesLimitInBytes",1000000) Document.all.FileSizeLimitInBytes.Value = _ GetRegistryDWORD(strWebClientKeyPath, _ "FileSizeLimitInBytes",50000000) Document.all.LocalServerTimeoutInSec.Value = _ GetRegistryDWORD(strWebClientKeyPath, _ "LocalServerTimeoutInSec",15) Document.all.SendReceiveTimeoutInSec.Value = _ GetRegistryDWORD(strWebClientKeyPath, _ "SendReceiveTimeoutInSec",60) Document.all.ServerNotFoundCacheLifeTimeInSec.Value = _ GetRegistryDWORD(strWebClientKeyPath, _ "ServerNotFoundCacheLifeTimeInSec",60) ' Get the text area values tmpArray = GetRegistryMULTISZ( _ strWebClientKeyPath,"AuthForwardServerList") For tmpCount = 0 To UBound(tmpArray) tmpString = tmpString & tmpArray(tmpCount) & vbTab Next If Len(tmpString)>0 Then Document.all.AuthForwardServerList.Value = _ Replace(Left(tmpString,Len(tmpString)-1),vbTab,vbCrLf) End If blnHasChanges = False End Sub ' ---------------------------------------- ' Save the settings in the registry. ' ---------------------------------------- Sub SetValues() On Error Resume Next ' Set the radio button values Call SetRegistryDWORD( _ strWebClientKeyPath, _ "BasicAuthLevel", _ GetRadioValue(Document.all.BasicAuthLevel)) Call SetRegistryDWORD( _ strWebClientKeyPath, _ "SupportLocking", _ GetRadioValue(Document.all.SupportLocking)) ' Set the text box values Call SetRegistryDWORD( _ strWebClientKeyPath, _ "InternetServerTimeoutInSec", _ Document.all.InternetServerTimeoutInSec.Value) Call SetRegistryDWORD( _ strWebClientKeyPath, _ "FileAttributesLimitInBytes", _ Document.all.FileAttributesLimitInBytes.Value) Call SetRegistryDWORD( _ strWebClientKeyPath, _ "FileSizeLimitInBytes", _ Document.all.FileSizeLimitInBytes.Value) Call SetRegistryDWORD( _ strWebClientKeyPath, _ "LocalServerTimeoutInSec", _ Document.all.LocalServerTimeoutInSec.Value) Call SetRegistryDWORD( _ strWebClientKeyPath, _ "SendReceiveTimeoutInSec", _ Document.all.SendReceiveTimeoutInSec.Value) Call SetRegistryDWORD( _ strWebClientKeyPath, _ "ServerNotFoundCacheLifeTimeInSec", _ Document.all.ServerNotFoundCacheLifeTimeInSec.Value) ' Set the text area values Call SetRegistryMULTISZ( _ strWebClientKeyPath, _ "AuthForwardServerList", _ Split(Document.all.AuthForwardServerList.Value,vbCrLf)) ' Prompt to restart the WebClient service If MsgBox("Do you want to restart the WebDAV Redirector " & _ "service so your settings will take effect?", _ vbQuestion Or vbYesNo Or vbDefaultButton2, _ "Restart WebDAV Redirector") = vbYes Then ' Restart the WebClient service. Call RestartWebClient() End If Call GetValues() End Sub ' ---------------------------------------- ' Start the WebClient service. ' ---------------------------------------- Sub RestartWebClient() On Error Resume Next Dim objWMIService,colServices,objService Document.All.TheBody.ClassName = "hide" Set objWMIService = GetObject( _ "winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2") Set colServices = objWMIService.ExecQuery( _ "Select * from Win32_Service Where Name='WebClient'") For Each objService in colServices objService.StopService() objService.StartService() Next Document.All.TheBody.ClassName = "show" End Sub ' ---------------------------------------- ' Retrieve a DWORD value from the registry. ' ---------------------------------------- Function GetRegistryDWORD( _ ByVal tmpKeyPath, _ ByVal tmpValueName, _ ByVal tmpDefaultValue) On Error Resume Next Dim tmpDwordValue If objRegistry.GetDWORDValue( _ HKEY_LOCAL_MACHINE, _ tmpKeyPath, _ tmpValueName, _ tmpDwordValue)=0 Then GetRegistryDWORD = CLng(tmpDwordValue) Else GetRegistryDWORD = CLng(tmpDefaultValue) End If End Function ' ---------------------------------------- ' Set a DWORD value in the registry. ' ---------------------------------------- Sub SetRegistryDWORD( _ ByVal tmpKeyPath, _ ByVal tmpValueName, _ ByVal tmpDwordValue) On Error Resume Next Call objRegistry.SetDWORDValue( _ HKEY_LOCAL_MACHINE, _ tmpKeyPath, _ tmpValueName, _ CLng(tmpDwordValue)) End Sub ' ---------------------------------------- ' Retrieve a MULTISZ value from the registry. ' ---------------------------------------- Function GetRegistryMULTISZ( _ ByVal tmpKeyPath, _ ByVal tmpValueName) On Error Resume Next Dim tmpMultiSzValue If objRegistry.GetMultiStringValue( _ HKEY_LOCAL_MACHINE, _ tmpKeyPath, _ tmpValueName, _ tmpMultiSzValue)=0 Then GetRegistryMULTISZ = tmpMultiSzValue Else GetRegistryMULTISZ = Array() End If End Function ' ---------------------------------------- ' Set a MULTISZ value in the registry. ' ---------------------------------------- Sub SetRegistryMULTISZ( _ ByVal tmpKeyPath, _ ByVal tmpValueName, _ ByVal tmpMultiSzValue) On Error Resume Next Call objRegistry.SetMultiStringValue( _ HKEY_LOCAL_MACHINE, _ tmpKeyPath, _ tmpValueName, _ tmpMultiSzValue) End Sub ' ---------------------------------------- ' Retrieve the value of a radio button group. ' ---------------------------------------- Function GetRadioValue(ByVal tmpRadio) On Error Resume Next Dim tmpCount For tmpCount = 0 To (tmpRadio.Length-1) If tmpRadio(tmpCount).Checked Then GetRadioValue = CLng(tmpRadio(tmpCount).Value) Exit For End If Next End Function ' ---------------------------------------- ' Set the value for a radio button group. ' ---------------------------------------- Sub SetRadioValue(ByVal tmpRadio, ByVal tmpValue) On Error Resume Next Dim tmpCount For tmpCount = 0 To (tmpRadio.Length-1) If CLng(tmpRadio(tmpCount).Value) = CLng(tmpValue) Then tmpRadio(tmpCount).Checked = True Exit For End If Next End Sub ' ---------------------------------------- ' ' ---------------------------------------- Sub Validate(tmpField) Dim tmpRegEx, tmpMatches Set tmpRegEx = New RegExp tmpRegEx.Pattern = "[0-9]" tmpRegEx.IgnoreCase = True tmpRegEx.Global = True Set tmpMatches = tmpRegEx.Execute(tmpField.Value) If tmpMatches.Count = Len(CStr(tmpField.Value)) Then If CDbl(tmpField.Value) => 0 And _ CDbl(tmpField.Value) =< 4294967295 Then Exit Sub End If End If MsgBox "Please enter a whole number between 0 and 4294967295.", _ vbCritical, "Validation Error" tmpField.Focus End Sub ' ---------------------------------------- ' ' ---------------------------------------- Sub BasicAuthWarning() MsgBox "WARNING:" & vbCrLf & vbCrLf & _ "Using Basic Authentication over non-SSL connections can cause " & _ "serious security issues. Usernames and passwords are transmitted " & _ "in clear text, therefore the use of Basic Authentication with " & _ "WebDAV is disabled by default for non-SSL connections. That " & _ "being said, this setting can override the default behavior for " & _ "Basic Authentication, but it is strongly discouraged.", _ vbCritical, "Basic Authentication Warning" End Sub ' ---------------------------------------- ' End of main code section. ' ---------------------------------------- </script> <style> body { color:#000000; background-color:#cccccc; font-family:'Segoe UI',Tahoma,Verdana,Arial; font-size:9pt; } fieldset { padding:10px; width:640px; } .button { width:150px; } .textbox { width:200px; height:22px; text-align:right; } .textarea { width:300px; height:50px; text-align:left; } .radio { margin-left:-5px; margin-top: -2px; } .hide { display:none; } .show { display:block; } select { width:300px; text-align:left; } table { border-collapse:collapse; empty-cells:hide; } h1 { font-size:14pt; } th { font-size:9pt; text-align:left; vertical-align:top; padding:2px; } td { font-size:9pt; text-align:left; vertical-align:top; padding:2px; } big { font-size:11pt; } small { font-size:8pt; } </style> </head> <body id="TheBody" class="hide"> <h1 align="center" id="TheTitle" style="margin-bottom:-20px;">WebDAV Redirector Settings</h1> <div align="center"> <p style="margin-bottom:-20px;"><i><small><b>Note</b>: See <a target="_blank" href="http://go.microsoft.com/fwlink/?LinkId=324291">Using the WebDAV Redirector</a> for additional details.</small></i></p> <form> <center> <table border="0" cellpadding="2" cellspacing="2" style="width:600px;"> <tr> <td style="width:600px;text-align:left"><fieldset title="Security Settings"> <legend> <b>Security Settings</b> </legend> These values affect the security behavior for the WebDAV Redirector.<br> <table style="width:600px;"> <tr title="Specifies whether the WebDAV Redirector can use Basic Authentication to communicate with a server."> <td style="width:300px"> <table border="0"> <tr> <td style="width:300px"><b>Basic Authentication Level</b></td> </tr> <tr> <td style="width:300px;"><span style="width:280px;padding-left:20px;"><small><i><b>Note</b>: Using basic authentication can cause <u>serious security issues</u> as the username and password are transmitted in clear text, therefore the use of basic authentication over WebDAV is disabled by default unless the connection is using SSL.</i></small></span></td> </tr> </table> </td> <td style="width:300px"> <table style="width:300px"> <tr> <td style="width:020px"><input class="radio" type="radio" value="0" name="BasicAuthLevel" onchange="VBScript:FlagChanges()" id="BasicAuthLevel0"></td> <td style="width:280px"><label for="BasicAuthLevel0">Basic Authentication is disabled</label></td> </tr> <tr> <td style="width:020px"><input class="radio" type="radio" value="1" checked name="BasicAuthLevel" onchange="VBScript:FlagChanges()" id="BasicAuthLevel1"></td> <td style="width:280px"><label for="BasicAuthLevel1">Basic Authentication is enabled for SSL web sites only</label></td> </tr> <tr> <td style="width:020px"><input class="radio" type="radio" value="2" name="BasicAuthLevel" onchange="VBScript:FlagChanges()" id="BasicAuthLevel2" onClick="VBScript:BasicAuthWarning()"></td> <td style="width:280px"><label for="BasicAuthLevel2">Basic Authentication is enabled for SSL and non-SSL web sites</label></td> </tr> </table> </td> </tr> <tr title="Specifies a list of local URLs for forwarding credentials that bypasses any proxy settings. (Note: This requires Windows Vista SP1 or later.)"> <td style="width:300px"> <table border="0"> <tr> <td style="width:300px"><b>Authentication Forwarding Server List</b></td> </tr> <tr> <td style="width:300px;"><span style="width:280px;padding-left:20px;"><small><i><b>Note</b>: Include one server name per line.</i></small></span></td> </tr> </table> </td> <td style="width:300px"><textarea class="textarea" name="AuthForwardServerList" onchange="VBScript:FlagChanges()"></textarea></td> </tr> <tr title="Specifies whether the WebDAV Redirector supports locking."> <td style="width:300px"><b>Support for WebDAV Locking</b></td> <td style="width:300px"> <table style="width:300px"> <tr> <td style="width:020px"><input class="radio" type="radio" value="1" checked name="SupportLocking" onchange="VBScript:FlagChanges()" id="SupportLocking1"></td> <td style="width:280px"><label for="SupportLocking1">Enable Lock Support</label></td> </tr> <tr> <td style="width:020px"><input class="radio" type="radio" value="0" name="SupportLocking" onchange="VBScript:FlagChanges()" id="SupportLocking0"></td> <td style="width:280px"><label for="SupportLocking0">Disable Lock Support</label></td> </tr> </table> </td> </tr> </table> </fieldset> </td> </tr> <tr> <td style="width:600px;text-align:left"><fieldset title="Time-outs"> <legend> <b>Time-outs and Maximum Sizes</b> </legend> These values affect the behavior for WebDAV Client/Server operations.<br> <table border="0" style="width:600px;"> <tr title="Specifies the connection time-out for the WebDAV Redirector uses when communicating with non-local WebDAV servers."> <td style="width:300px"><b>Internet Server Time-out</b> <small>(In Seconds)</small></td> <td style="width:300px"><input class="textbox" type="text" name="InternetServerTimeoutInSec" onchange="VBScript:FlagChanges()" onblur="VBScript:Validate(Me)" value="30"></td> </tr> <tr title="Specifies the connection time-out for the WebDAV Redirector uses when communicating with a local WebDAV server."> <td style="width:300px"><b>Local Server Time-out</b> <small>(In Seconds)</small></td> <td style="width:300px"><input class="textbox" type="text" name="LocalServerTimeoutInSec" onchange="VBScript:FlagChanges()" onblur="VBScript:Validate(Me)" value="15"></td> </tr> <tr title="Specifies the time-out in seconds that the WebDAV Redirector uses after issuing a request."> <td style="width:300px"><b>Send/Receive Time-out</b> <small>(In Seconds)</small></td> <td style="width:300px"><input class="textbox" type="text" name="SendReceiveTimeoutInSec" onchange="VBScript:FlagChanges()" onblur="VBScript:Validate(Me)" value="60"></td> </tr> <tr title="Specifies the period of time that a server is cached as non-WebDAV by the WebDAV Redirector. If a server is found in this list, a fail is returned immediately without attempting to contact the server."> <td style="width:300px"><b>Server Not Found Cache Time-out</b> <small>(In Seconds)</small></td> <td style="width:300px"><input class="textbox" type="text" name="ServerNotFoundCacheLifeTimeInSec" onchange="VBScript:FlagChanges()" onblur="VBScript:Validate(Me)" value="60"></td> </tr> <tr title="Specifies the maximum size in bytes that the WebDAV Redirector allows for file transfers."> <td style="width:300px"><b>Maximum File Size</b> <small>(In Bytes)</small></td> <td style="width:300px"><input class="textbox" type="text" name="FileSizeLimitInBytes" onchange="VBScript:FlagChanges()" onblur="VBScript:Validate(Me)" value="50000000"></td> </tr> <tr title="Specifies the maximum size that is allowed by the WebDAV Redirector for all properties on a specific collection."> <td style="width:300px"><b>Maximum Attributes Size</b> <small>(In Bytes)</small></td> <td style="width:300px"><input class="textbox" type="text" name="FileAttributesLimitInBytes" onchange="VBScript:FlagChanges()" onblur="VBScript:Validate(Me)" value="1000000"></td> </tr> </table> </fieldset> </td> </tr> <tr> <td style="text-align:center"> <table border="0"> <tr> <td style="text-align:center"><input class="button" type="button" value="Apply Settings" onclick="VBScript:SetValues()"> <td style="text-align:center"><input class="button" type="button" value="Reset Values" onclick="VBScript:ResetApplication()"> <td style="text-align:center"><input class="button" type="button" value="Exit Application" onclick="VBScript:ExitApplication()"> </tr> </table> </td> </tr> </table> </center> </form> </div> </body> </html>
As with the last version of this HTML Application, you will need to run this application as an administrator in order to save the settings to the registry and restart the WebDAV Redirector service.
Have fun! ;-]
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
17 September 2013 • by Bob • IIS, WebDAV, URL Rewrite
I had an interesting WebDAV question earlier today that I had not considered before: how can someone create a "Blind Drop Share" using WebDAV? By way of explanation, a Blind Drop Share is a path where users can copy files, but never see the files that are in the directory. You can setup something like this by using NTFS permissions, but that environment can be a little difficult to set up and maintain.
With that in mind, I decided to research a WebDAV-specific solution that didn't require mangling my NTFS permissions. In the end it was pretty easy to achieve, so I thought that it would make a good blog for anyone who wants to do this.
NTFS permissions contain access controls that configure the directory-listing behavior for files and folders; if you modify those settings, you can control who can see files and folders when they connect to your shared resources. However, there are no built-in features for the WebDAV module which ships with IIS that will approximate the NTFS behavior. But that being said, there is an interesting WebDAV quirk that you can use that will allow you to restrict directory listings, and I will explain how that works.
WebDAV uses the PROPFIND
command to retrieve the properties for files and folders, and the WebDAV Redirector will use the response from a PROPFIND
command to display a directory listing. (Note: Official WebDAV terminology has no concept of files and folders, those physical objects are respectively called Resources and Collections in WebDAV jargon. But that being said, I will use files and folders throughout this blog post for ease of understanding.)
In any event, one of the HTTP headers that WebDAV uses with the PROPFIND
command is the Depth
header, which is used to specify how deep the folder/collection traversal should go:
PROPFIND
command for the root of your website with a Depth:0
header/value, you would get the properties for just the root directory - with no files listed; a Depth:0
header/value only retrieves properties for the single resource that you requested.PROPFIND
command for the root of your website with a Depth:1
header/value, you would get the properties for every file and folder in the root of your website; a Depth:1
header/value retrieves properties for the resource that you requested and all siblings.PROPFIND
command for the root of your website with a Depth:infinity
header/value, you would get the properties for every file and folder in your entire website; a Depth:infinity
header/value retrieves properties for every resource regardless of its depth in the hierarchy. (Note that retrieving directory listings with infinite depth are disabled by default in IIS 7 and IIS 8 because it can be CPU intensive.)By analyzing the above information, it should be obvious that what you need to do is to restrict users to using a Depth:0
header/value. But that's where this scenario gets interesting: if your end-users are using the Windows WebDAV Redirector or other similar technology to map a drive to your HTTP website, you have no control over the value of the Depth
header. So how can you restrict that?
In the past I would have written custom native-code HTTP module or ISAPI filter to modify the value of the Depth
header; but once you understand how WebDAV works, you can use the URL Rewrite module to modify the headers of incoming HTTP requests to accomplish some pretty cool things - like modifying the values WebDAV-related HTTP headers.
Here's how I configured URL Rewrite to set the value of the Depth
header to 0
, which allowed me to create a "Blind Drop" WebDAV site:
![]() |
Click image to expand |
![]() |
Click image to expand |
![]() |
Click image to expand |
![]() |
Click image to expand |
![]() |
Click image to expand |
![]() |
Click image to expand |
![]() |
![]() |
Click image to expand |
![]() |
Click image to expand |
![]() |
Click image to expand |
If all of these changes were saved to your applicationHost.config file, the resulting XML might resemble the following example - with XML comments added by me to highlight some of the major sections:
<location path="Default Web Site"> <system.webServer> <-- Start of Security Settings --> <security> <authentication> <anonymousAuthentication enabled="false" /> <basicAuthentication enabled="true" /> </authentication> <requestFiltering> <fileExtensions applyToWebDAV="false" /> <verbs applyToWebDAV="false" /> <hiddenSegments applyToWebDAV="false" /> </requestFiltering> </security> <-- Start of WebDAV Settings --> <webdav> <authoringRules> <add roles="administrators" path="*" access="Read, Write, Source" /> </authoringRules> <authoring enabled="true"> <properties allowAnonymousPropfind="false" allowInfinitePropfindDepth="true"> <clear /> <add xmlNamespace="*" propertyStore="webdav_simple_prop" /> </properties> </authoring> </webdav> <-- Start of URL Rewrite Settings --> <rewrite> <rules> <rule name="Modify Depth Header" enabled="true" patternSyntax="Wildcard"> <match url="*" /> <serverVariables> <set name="HTTP_DEPTH" value="0" /> </serverVariables> <action type="None" /> </rule> </rules> <allowedServerVariables> <add name="HTTP_DEPTH" /> </allowedServerVariables> </rewrite> </system.webServer> </location>
In all likelihood, some of these settings will be stored in your applicationHost.config file, and the remaining settings will be stored in the web.config file of your website.
If you did not have the URL Rewrite rule in place, or if you disabled the rule, then your web server might respond like the following example if you used the WebDAV Redirector to map a drive to your website from a command prompt:
C:\>net use z: http://www.contoso.com/ Enter the user name for 'www.contoso.com': www.contoso.com\robert Enter the password for www.contoso.com: The command completed successfully. C:\>z: Z:\>dir Volume in drive Z has no label. Volume Serial Number is 0000-0000 Directory of Z:\ 09/16/2013 08:55 PM <DIR> . 09/16/2013 08:55 PM <DIR> .. 09/14/2013 12:39 AM <DIR> aspnet_client 09/16/2013 08:06 PM <DIR> scripts 09/16/2013 07:55 PM 66 default.aspx 09/14/2013 12:38 AM 98,757 iis-85.png 09/14/2013 12:38 AM 694 iisstart.htm 09/16/2013 08:55 PM 75 web.config 4 File(s) 99,592 bytes 8 Dir(s) 956,202,631,168 bytes free Z:\>
However, when you have the URL Rewrite correctly configured and enabled, connecting to the same website will resemble the following example - notice how no files or folders are listed:
C:\>net use z: http://www.contoso.com/ Enter the user name for 'www.contoso.com': www.contoso.com\robert Enter the password for www.contoso.com: The command completed successfully. C:\>z: Z:\>dir Volume in drive Z has no label. Volume Serial Number is 0000-0000 Directory of Z:\ 09/16/2013 08:55 PM <DIR> . 09/16/2013 08:55 PM <DIR> .. 0 File(s) 0 bytes 2 Dir(s) 956,202,803,200 bytes free Z:\>
Despite the blank directory listing, you can still retrieve the properties for any file or folder if you know that it exists. So if you were to use the mapped drive from the preceding example, you could still use an explicit directory command for any object that you had uploaded or created:
Z:\>dir default.aspx Volume in drive Z has no label. Volume Serial Number is 0000-0000 Directory of Z:\ 09/16/2013 07:55 PM 66 default.aspx 1 File(s) 66 bytes 0 Dir(s) 956,202,799,104 bytes free Z:\>dir scripts Volume in drive Z has no label. Volume Serial Number is 0000-0000 Directory of Z:\scripts 09/16/2013 07:52 PM <DIR> . 09/16/2013 07:52 PM <DIR> .. 0 File(s) 0 bytes 2 Dir(s) 956,202,799,104 bytes free Z:\>
The same is true for creating directories and files; you can create them, but they will not show up in the directory listings after you have created them unless you reference them explicitly:
Z:\>md foobar Z:\>dir Volume in drive Z has no label. Volume Serial Number is 0000-0000 Directory of Z:\ 09/16/2013 11:52 PM <DIR> . 09/16/2013 11:52 PM <DIR> .. 0 File(s) 0 bytes 2 Dir(s) 956,202,618,880 bytes free Z:\>cd foobar Z:\foobar>copy NUL foobar.txt 1 file(s) copied. Z:\foobar>dir Volume in drive Z has no label. Volume Serial Number is 0000-0000 Directory of Z:\foobar 09/16/2013 11:52 PM <DIR> . 09/16/2013 11:52 PM <DIR> .. 0 File(s) 0 bytes 2 Dir(s) 956,202,303,488 bytes free Z:\foobar>dir foobar.txt Volume in drive Z has no label. Volume Serial Number is 0000-0000 Directory of Z:\foobar 09/16/2013 11:53 PM 0 foobar.txt 1 File(s) 0 bytes 0 Dir(s) 956,202,299,392 bytes free Z:\foobar>
That wraps it up for today's post, although I should point out that if you see any errors when you are using the WebDAV Redirector, you should take a look at the Troubleshooting the WebDAV Redirector section of my Using the WebDAV Redirector article; I have done my best to list every error and resolution that I have discovered over the past several years.
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
06 September 2013 • by Bob • Rants
Many years ago - more years than I care to admit - I worked in the IT department for a local community college in Tucson, AZ. I worked with a great bunch of people during my time at that institution, and now that I have returned to Tucson, it's fun to get reacquainted with my old colleagues and catch up on what's been happening in everyone's lives.
With that in mind, I recently had the opportunity to meet one of my old coworkers for lunch. Our destination was near the University of Arizona, so I parked my car in one of the university's parking garages and set out across the university campus on foot. As I was walking past the mathematics buildings, I happened to overhear one side of an exasperated conversation that a young twenty-something was having on her cell phone. The main source of her consternation appeared to be: "My class has a test in it every day, and the professor never teaches us what's on the test!"
My immediate thought was: "That's good; you're supposed to study and learn the material, then you'll already know what's on the test." This made me laugh first, but after further analysis of the situation, I don't think that it's all that funny. I think that this twenty-something's expectations are a byproduct of today's standardized testing - she expects to be taught what's on the test instead of actually learning the material.
If that's the case, then it's a pretty bad testimony about the state of education in America today.
30 August 2013 • by Bob • Art, Photography
Several years ago my wife and I entered the Leavenworth Half-Marathon; we had recently both lost weight, and we wanted to do something big to test our new-found health. Because the half-marathon takes place in the Fall, I knew that the leaves on the trees would be changing colors, so I brought my DSLR camera and tripod with me.
On our way back to Seattle after the marathon, we passed by several groves of trees on either side of the road that were displaying a dizzying array of radiant colors. As we approached a road that was announcing a new housing development that was coming soon, I thought this would make a great place to take photos - especially before the developers cut down all of the amazingly colorful foliage to build houses.
As we turned off the main highway between Leavenworth and Seattle, we stopped on a newly-graveled road that led to the future construction sites. To the east of the road was a veritable wall of brilliantly-colored trees, while to the west was an unfenced field with the run-down remnants of a farmhouse and barn.
I got my camera gear out of the car, while Kathleen settled down in the front seat of our car to take a quick nap. I walked along the gravel road, and I stopped periodically to set up my tripod and take a few photos.
Nature did little to disappoint me; it seemed that everywhere I turned I was surrounded by an eruption of vibrant color. My only regret was that I wasn't a better photographer with skills that could capture what my eyes were actually seeing.
I had been careful to stay on the road as I took my photographs for no particular reason; there were no fences that prevented me from crossing into the woods or the nearby field - I simply felt no need to leave the road to line up any of my camera shots. In hindsight, I suppose that I didn't want to track a bunch of mud back to the car.
After a half-hour or so, I had satisfied my inner shutterbug, and I packed my equipment to leave. As I walked back to the car, I realized that if I walked into the field on the west side of the road, I could line up a photo with the barn in the foreground and the grove of trees in the distance.
I have to be honest - there are hundreds if not thousands of photographers who take endless numbers of barn photographs, and that's simply not my style. But on this one occasion, I thought this particular arrangement might result in a decent photo or two. With this in mind, I set down my camera bag in the middle of the road near our car, and I walked a hundred yards or so into the field near the barn.
I set up my camera and tripod, then I lined up a shot, and I set my timer to take a single image. As I heard the shutter click, I happened to notice someone walking towards me from the general vicinity of the dilapidated farmhouse. As the person drew nearer I realized that it wasn't Kathleen, but the stranger waved to me and I waved back cordially. I turned to look at my camera when the stranger's voice was suddenly audible, and I heard him yell, "What the @#$% do you think you're doing!!!"
At that point, I realized that the situation was going to be bad.
Very bad.
As he walked up to me, he swung his arms widely in the air as he screamed a tirade of expletives that made little sense, punctuated by occasional moments of clarity when his threats of beating me to a pulp were all-too intelligible and disturbingly believable.
My would-be assailant drew to a stop within inches of my face, and he continued to hurl fiery verbal spitballs of ill will as I stepped back instinctively. I apologized profusely for whatever it was that I must have done, to which my aggressor shouted that I was trespassing. I apologized again, and I replied that this was my fault entirely; I had seen no signs nor fences to indicate that the property was privately owned. I hastily explained that I thought the land was unoccupied prior to the commencement of the impending development project, while my infuriated companion continuously mocked my every word.
In my former career as a technical support engineer, I had dealt with more than my fair share of angry and unreasonable customers, and I was drawing on every ounce of experience to try everything within my power to diffuse the situation, but nothing seemed to work. My assailant continued to scream at me as I said that I would take my things and leave. As I reached for my camera, my belligerent host screamed, "Don't you touch me!!!", and he jumped back several feet. I explained that I was simply going to pack up my camera, to which he angrily responded, "It's on my land!!! It belongs to me now!!!"
Up to this point in the conversation I had been on the defensive. (Or more accurately, I had been in retreat.) But once he mentioned keeping my camera equipment, I switched gears and strongly remarked, "No - this doesn't belong to you, and I'm taking it with me."
My sudden change in tone prompted a different reaction from my antagonist - he demanded that we call the development company so that I could explain why I was trespassing. I agreed to his terms; after all, I probably was trespassing, even if I had done so unwittingly. But I also thought that whomever I spoke to at the development company would have to be able to participate in a more reasonable dialogue than my enraged escort.
As the two of us walked towards the farmhouse, I had no intention of actually going inside his derelict dwelling. (I've seen too many horror movies for that.) But I suddenly remembered that I had left my camera bag sitting in the middle of the road, and I changed course to recover it. As I did so, my hostile host shouted, "Where are you going???"
I explained that I was going to retrieve my camera bag, but I was now near enough to the car for the shouting to wake Kathleen. As she sat up in our car's front seat, my unwelcome companion suddenly noticed her, and it visibly dawned on him that he was outnumbered. (Even if neither Kathleen nor I were ready to provoke an all-out fight.)
Despite his earlier aggressive stance, my would-be attacker now backed away rapidly, and he yelled at me to leave as fast as possible, and he demanded that I call the property company on my own so that I could explain why I was trespassing. (I agreed to make the call, but of course I never actually did.)
As Kathleen and I drove away, it took a while for the adrenalin to burn off and my nerves to mend. Once we had arrived home safely, I looked through my collection of photos from earlier in the day. I had a few nice photos of colorful leaves, but what I really wanted to see was whether the solitary photo for which I had risked life and limb was worth the potential hazards that I had endured.
I will let you be the judge... here is the actual image:
I think this is the last time that I will try to photograph a barn.
29 August 2013 • by Bob • Family, Rants
Earlier today I saw a link to an article by Allison Benedikt titled If You Send Your Kid to Private School, You Are a Bad Person. With a catchy title like that, I couldn't resist following the link in order to read what the author had to say about parenthood.
Before I continue, I should point out two important facts: 1) my children's formative years were spent in a mixture of both public and private education, and 2) at the time that Ms. Benedikt published her editorial piece, neither of her children were old enough for school, so any of her admittedly-judgmental opinions were made from the relative safety of someone who has never had to face the harsh realities about the topics which she was discussing. Ms. Benedikt's self-admitted ignorance at the hands of public educators provides little evidentiary support for her thesis statement, and unfortunately she is too blinded by her own hubris to realize it. No - it is not the well-meaning parents of children in private school who are bad people, it is self-righteous and judgmental people like her who are bad people.
I vehemently disagree with Ms. Benedikt's overall premise; it is not the parents who have realized that public education is a failing system who are ruining one of our nation's most-essential institutions – our present educational system is ruining itself. Most parents with school-age children are all-too-aware that public education is depriving their children of knowledge that is necessary to succeed academically. A perfect example is when the overly-vocal and seldom-intelligent actor Matt Damon abandoned his idealistic rhetoric demanding public education for everyone else and placed his own children in private schools. At some point in the not-too-distant future, Ms. Benedikt will be faced with the choice of whether to sacrifice her own children for the sake of her principles, or to choose what is best for her children based on her maternal instincts.
I also passionately object to anyone who insists that I should not turn my back on any failing system and subject my children to a negative environment in the hopes that the system will improve for future generations. My children are not a social experiment, nor am I willing to gamble with their lives. I do not care if Ms. Benedikt and her ilk intend to fix the schools of the future if the methods to achieve those goals cheat my children in the present.
By the way, each of my three children started in public school until my wife and I realized how poorly they were being educated. After three failed attempts with public schools, we moved each child into private school for their primary education to give them a better foundation, and then we returned them to public schools for secondary education. This system helped each of our children immensely, all of whom have now graduated college and embarked on successful careers.
Without getting deeper into an unintentional political rant, this private versus public school debate illustrates much of what is wrong with most socialistic policies; many "public" institutions fail because they become so weighed down by unnecessary bureaucracy that they can barely serve their primary purpose. Public education is not failing because parents are pulling their children out; public education is failing because we do not pay our educators enough, and we do not provide adequate resources for our schools. While it is true that our taxpayer dollars are simply not paying enough to take care of all society's educational expenses, we also have a system that is so top-heavy with needless bureaucrats and inundated with policies which occupy entirely too much time. As a result, our nation is not seeing a sufficient return on investment. What's more, the measures that the Department of Education has implemented to standardize education and hold teachers accountable for their results have been complete failures.
But that being said, here are a few of my grievances with the various excuses that I have personally heard from public educators:
I'll get off my soapbox now, but I'd like to discuss one final point – as I mentioned earlier, Ms. Benedikt's children are not yet old enough to attend school, which prevents me from taking any of her self-righteous drivel seriously. In my opinion, her lack of personal experience in this matter disqualifies her from passing judgment on parents who actually have to decide what is best for their children; close-minded and emotionally detached fools with no personal stake in this debate should be ineligible to weigh in on the issue.
28 August 2013 • by Bob • Arizona, Humor
Note: A friend had reposted the following list on the Internet... I love these kinds of lists, because they always provide you with a chance to laugh at your surroundings in a way that only someone with intimate knowledge of the area can appreciate.
You Know You're From Arizona When...
Ah, so true, so true... ;-)
20 August 2013 • by Bob • IIS Express, ASP.NET, Web API, IIS Express, ASP.NET, Web API
I've been playing around with Web API a lot recently, and I've found that it's a really powerful and elegant way to create internet-based applications. After writing several server-side Web API applications, I thought that it would be fun to write a Windows Phone 8 application that used Web API to communicate with one of my server-side applications.
I was using the Visual Studio 2013 Preview to write my Web API application, and by default Visual Studio 2012 and later use IIS Express for the development web server on http://localhost with a random port. With this in mind, I thought that it would be trivial to create a Windows Phone 8 application that would be able to send HTTP GET requests to http://localhost to download data. This seemed like an easy thing to do, but it turned out to be considerably more difficult than I had assumed, so I thought that I would dedicate a blog post to getting this scenario to work.
I should point out that I stumbled across the following article while I was getting my environment up and working, but that article had a few steps that didn't apply to my environment, and there were a few things that were required for my environment that were missing from the article:
http://msdn.microsoft.com/en-us/library/windowsphone/develop/jj684580.aspx
In addition, there were a few things that I needed that might not apply to everyone, and I'll try to point those out as I go along.
First things first, you need to make sure that you have a physical computer that meets the system requirements for the Windows Phone 8 SDK that are posted at the following URL:
http://www.microsoft.com/en-us/download/details.aspx?id=35471
Here is an annotated copy of the system requirements list at the time that I wrote this blog:
The first time that I launched my Web API application and tried to connect to it from the Windows Phone 8 Emulator (WP8E), I used "http://localhost" as the domain where my Web API application was hosted. This was a silly mistake on my part; the WP8E runs in Hyper-V, and the WP8E believes that it is a separate computing device than your host computer, so using "http://localhost" in the WP8E meant that it was trying to browse to itself, not the host machine. (Duh.)
My computer was joined to a domain, and our domain uses IP Security (IPSEC) for obvious reasons. That being said, the Windows Phone 8 Emulator is not going to use IPSEC to connect to the host machine, so I needed to find a way around IPSEC.
Our IT department allows us to make a domain-joined machine as a boundary machine to get past this problem, but I decided to set up a new, physical machine from scratch for my development testing.
After you install the Windows Phone 8 SDK with the Windows Phone 8 Emulator (WP8E), you will have a new virtual machine in Hyper-V with a name like "Emulator WVGA 512MB"; this is the actual WP8E instance.
You should also have a new virtual switch named "Windows Phone Emulator Internal Switch" in the Hyper-V Virtual Switch Manager; WP8E will use this virtual switch to communicate with your host machine. (This virtual switch was missing on one of the systems where I was doing some testing, so I had to add it manually.)
Since the Hyper-V machine for the Windows Phone 8 Emulator (WP8E) and IIS Express will be communicating over your network, you will need to make sure that the Windows Firewall is not blocking the communication. I attempted to add an exception for IIS Express to the Windows Firewall, but that did not seem to have any effect - I had to actually disable the Windows Firewall on my development machine to get my environment working. (Note that it is entirely possible that I needed to configure something else in my Windows Firewall settings in order to allow my environment to work without disabling Windows Firewall, but I couldn't find it, and it was far easier in the short term to just disable Windows Firewall for the time being.)
It's entirely possible that the Windows Phone 8 Emulator (WP8E) always uses a 169.254.nnn.nnn IP address on the "Windows Phone Emulator Internal Switch", but I needed to make sure which IP address range to use when configuring IIS Express.
The way that I chose to do this was to drop the following code inside my WP8E application and I stepped through it in a debugger to tell me the IP addresses that WP8E had assigned for each network interface:
var hostnames = Windows.Networking.Connectivity.NetworkInformation.GetHostNames(); foreach (var hn in hostnames) { if (hn.IPInformation != null) { string ipAddress = hn.DisplayName; } }
I had two IP addresses for the WP8E: one in the 192.168.nnn.nnn IP address range, and another the 169.254.nnn.nnn IP address range. Once I verified that one of my IP addresses was in the 169.254.nnn.nnn range, I was able to pick an IP address in that range for IIS Express.
Once you have verified the IP address range that the Windows Phone 8 Emulator (WP8E) is using, you can either pick a random IP address within that range to use with IIS Express, or you can use the default IP address for the Windows Phone Emulator Internal Switch. To verify the default IP address, you would need to open a command prompt and run ipconfig, then look for the Internal Ethernet Port Windows Phone Emulator Internal Switch:
C:\>ipconfig
Windows IP Configuration Ethernet adapter vEthernet (Internal Ethernet Port Windows Phone Emulator Internal Switch): Connection-specific DNS Suffix . : Link-local IPv6 Address . . . . . : fe80::81d3:c1c7:307b:d732%11 IPv4 Address. . . . . . . . . . . : 169.254.80.80 Subnet Mask . . . . . . . . . . . : 255.255.0.0 Default Gateway . . . . . . . . . : Ethernet adapter vEthernet (Intel(R) 82579LM Gigabit Network Connection Virtual Switch): Connection-specific DNS Suffix . : Link-local IPv6 Address . . . . . : fe80::3569:3387:bb3f:583b%8 IPv4 Address. . . . . . . . . . . : 192.168.1.72 Subnet Mask . . . . . . . . . . . : 255.255.255.0 Default Gateway . . . . . . . . . : 192.168.1.1 C:\> |
If you decide to use a custom IP address in the range that your Windows Phone 8 Emulator is using, you would need to enter that IP address in the IPv4 TCP/IP settings for the Internal Ethernet Port Windows Phone Emulator Internal Switch:
![]() |
(Click the image to expand it.) |
Once you have the IP address that you intend to use, you will need to add that to your IIS Express settings. There are two easy ways to do this, both of which require administrative privileges on your system:
Method #1: Use AppCmd from an elevated command prompt
Open an elevated command prompt session by right-clicking the Command Prompt icon and choosing Run as administrator, then enter the following commands:
cd "%ProgramFiles%\IIS Express"
appcmd.exe set config -section:system.applicationHost/sites /+"[name='WebApplication1'].bindings.[protocol='http',bindingInformation='169.254.21.12:80:']" /commit:apphost
Where WebApplication1 is the name of your Web API application and 169.254.21.12 is the IP address that you chose for your testing.
Method #2: Manually edit ApplicationHost.config
Open Windows Notepad as an administrator, and open the "ApplicationHost.config" file for IIS Express for editing. By default this file should be located at "%UserProfile%\Documents\IISExpress\config\ApplicationHost.config".
Locate the code for the website of your Web API application; this should resemble something like the following, where WebApplication1 is the name of your Web API application:
<site name="WebApplication1" id="1" serverAutoStart="true"> <application path="/"> <virtualDirectory path="/" physicalPath="%IIS_SITES_HOME%\WebApplication1" /> </application> <bindings> <binding protocol="http" bindingInformation=":54321:localhost" /> </bindings> </site>
Copy the existing binding and paste it below the original entry, then change the binding to resemble the following example, where 169.254.21.12 is the IP address that you chose for your testing:
<site name="WebApplication1" id="1" serverAutoStart="true"> <application path="/"> <virtualDirectory path="/" physicalPath="%IIS_SITES_HOME%\WebApplication1" /> </application> <bindings> <binding protocol="http" bindingInformation=":54321:localhost" /> <binding protocol="http" bindingInformation="169.254.21.12:80:" /> </bindings> </site>
Save the file and close Windows Notepad.
Once you have configured IIS Express to use an internal IP address, you need to specify that IP address for your Windows Phone 8 application. The following example shows what this might look like:
public void LoadData() { if (this.IsDataLoaded == false) { this.Items.Clear(); WebClient webClient = new WebClient(); webClient.Headers["Accept"] = "application/json"; webClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(webClient_DownloadStringCompleted); webClient.DownloadStringAsync(new Uri(@"http://169.254.21.12/api/TodoList")); } }
Where 169.254.21.12 is the IP address that you chose for your testing.
It is essential that you configure your proxy settings so that the IP address of your Web API application will be considered an internal network address; otherwise all of the requests from the Windows Phone 8 Emulator (WP8E) will attempt to locate your Web API application on the Internet, which will fail.
The WP8E will use the proxy settings from your Windows Internet options, which are the same settings that are used by Internet Explorer. This means that you can either set your proxy settings through the Windows Control Panel by using the Internet Options feature, or you can use Internet Explorer's Tools -> Internet Options menus.
Once you have the Internet Options dialog open, click the Connections tab, then click the LAN settings button.
There are a few ways that you can specify your IP address as internal:
Method #1: Specify your proxy server, then click the Advanced button and add your internal IP address to the list of exceptions:
Method #2: If you do not need actual Internet access during your testing, you can specify "localhost" as the proxy server, then click the Advanced button and add your internal IP address to the list of exceptions:
Method #3: Specify your internal IP address as the proxy server; it isn't really a proxy server, of course, but this will keep the requests internal.
In order for IIS Express to register a binding with HTTP.SYS on an IP address other than 127.0.0.1, you need to run IIS Express using an elevated session. There are two easy ways to do this:
Method #1: Launch IIS Express from Visual Studio as an administrator
Start Visual Studio as an administrator by right-clicking the Visual Studio icon and choosing Run as administrator. Once Visual Studio has opened, you can open your Web API project and hit F5 to launch IIS Express.
Method #2: Launch IIS Express from an elevated command prompt
Open an elevated command prompt session by right-clicking the Command Prompt icon and choosing Run as administrator, then enter the following commands:
cd "%ProgramFiles%\IIS Express"
iisexpress.exe /site:WebApplication1
Where WebApplication1 is the name of your Web API application.
As I pointed out in my opening statements for this blog, getting the Windows Phone 8 Emulator to communicate with a Web API application on the same computer was not as easy as I would have thought, but all of the steps made sense once I had all of the disparate technologies working together.
Have fun! ;-]
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/