Just a short, simple blog for Bob to share his thoughts.
09 January 2014 • by Bob • Rants
At last night's 74th Golden Globes Awards, Meryl Streep had the audacity to compare the occasional inconveniences of Hollywood elites to the suffering of immigrants when she was delivering an acceptance speech for yet another unnecessary award. However, my sarcasm and rhetoric cannot do justice to just how out-of-touch Ms. Streep is with reality; here are the ridiculous thoughts she actually uttered out loud before a crowded auditorium:
"All of us in this room, really, belong to the most-vilified segments in American society right now. Think about it: Hollywood, Foreigners, and the Press."
First of all, it simply amazes me that Ms. Streep cannot comprehend the irony of equating whatever fake oppression she believes she is experiencing with the real-life suffering of others while standing onstage at another over-the-top example of Hollywood's infatuation with itself. Awards shows like the Golden Globes are - of course - nothing more than another opportunity for self-indulged entertainers to fall in love with themselves all over again. Judging by the number of times per year that Hollywood entertainers need to pat themselves on the back for doing their jobs, you quickly get the impression that they are anything but a vilified segment of American society. (Hollywood types are, however, incredibly insecure cry-babies with delusions of grandeur, but I digress.)
Think about it, no other career field - not even professional sports - has anywhere near the number of awards shows that Hollywood film, television, and music entertainers have for themselves every year. Here is only a partial list:
AACTA Awards, Academy Awards (The Oscars), Academy of Country Music Awards, ACTRA Awards, Alternative Press Music Awards, Amanda Awards, American Academy of Arts and Letters Gold Medal in Music, American Country Awards, American Country Countdown Awards, American Music Award, ARIA Music Awards, ASCAP awards , Atlanta International Film Festival, Awit Awards, Back Stage Garland Awards, BAFTA Anthony Asquith Award, BET Awards, Billboard Awards, Billboard Music Awards, Brit Awards, British Academy Film Awards, British Academy Television Awards, British Composer Awards, Canadian Cinema Editors Awards, Canadian Screen Awards, Cannes Lions International Advertising Festival awards, CMT Music Awards, Comet, Country Music Association Awards, Country Music Awards of Australia, Critics' Choice Movie Awards, Critics' Choice Television Awards, Directors Guild of Canada Awards, Distinguished Service to Music Medal, Donaldson Awards, Drama-Logue Awards, Drama Desk Awards, Drama League Awards, Edward R. Murrow Awards, Emmy Awards (Creative Arts), Emmy Awards (Daytime), Emmy Awards (Primetime), Emmy Awards (Sports), European Film Awards, Evening Standard Awards, Filipino Academy of Movie Arts and Sciences Awards, Filmfare Awards, FIPA awards, Game Show Awards, George Peabody Medal, GLAAD Media Awards, GLAAD Media Awards, Gold Badge Awards, Golden Bear, Golden Globe Awards, Golden Leopard, Golden Lion, Golden Melody Awards, Golden Nymph Awards, Golden Orange, Goldene Kamera, Governors Awards, Goya Awards, Gracie Allen Awards, Grammy Awards, Grand Prix du Disque, Grawemeyer Award for Music Composition, Grimme Preis, Handel Music Prize, Herbert von Karajan Music Prize, Hollywood Walk of Fame, Humanitas Prize, Independent Spirit Awards, Indonesian Film Festival, International Indian Film Academy Awards, Iris Awards, Ivor Novello Awards, Juno Awards, LA Weekly Theatre Awards, Latin Grammy Award, Laurence Olivier Awards, Leo Awards, Logie Awards, Lola Awards, Los Angeles Drama Critics Circle Award, Los Premios MTV Latinoamérica, Lucille Lortel Awards, Léonie Sonning Music Prize, MOBO Awards, MTV Africa Music Awards, MTV Asia Awards, MTV Australia Awards, MTV Europe Music Awards, MTV Movie & TV Awards, MTV Video Music Awards, National Dance Awards, National Film Awards, National Television Awards, New York Drama Critics' Circle Awards, New York Film Festival, Obie Awards, Otaka Prize, Outer Critics Circle Awards, Ovation Awards, Palme d'Or (Cannes Film Festival), Peabody Awards, Polar Music Prize, Praemium Imperiale, Prix de Rome, Pulitzer Prize for Drama, Pulitzer Prize for Music, Royal Television Society Awards, San Diego Film Awards, Sanremo Music Festival, Sarah Siddons Award, Screen Actors Guild Awards, Shooting Stars Award, Sibelius Prize, Stellar Awards, Suntory Music Award, Theatre World Award, Tony Awards, Young Artist Awards, YouTube Music Awards, etc., etc., etc.
And these awards are simply for doing their jobs. While I recognize the fact that such awards are ostensibly being given to those who did the best job for the year, it's still an award for doing something that often does not require that much skill. Seriously. It takes very little talent to pretend to be someone else for a few days per year. The guy who recently fixed the air conditioner in my house has arguably more skills than many of the Hollywood A-Listers. But then again, the guy who fixed my air conditioner doesn't get any awards for doing his job; he gets paid. Of course, he probably only made a few hundred dollars while working for me, whereas Ms. Streep makes millions of dollars every time she steps in front of the cameras to do something that any number of struggling actors could undoubtedly do better.
And yet Ms. Streep is oblivious to just how ridiculous she sounds when she compares her life of awards shows, exorbitant salaries, first class jet-setting around the globe, designer gowns, luxury hotels, and palatial mansions to the sufferings of refugees and immigrants. I find it difficult to believe how someone - how anyone - could be that self-absorbed.
Contrary to her feelings on the matter, Ms. Streep is not a member of a vilified segment of American society; but she should be.
31 December 2013 • by Bob • Music
I was watching the Rush Clockwork Angels Tour on DVD earlier today, and the video reminded me of a story that illustrates why I have always liked Rush, and why they have always been an atypical band.
Back in the early 1980s I saw Rush in concert several times; with each new Rush album would come a new Rush tour, and I caught every Rush show that I could. On one occasion, (I believe it was the Power Windows tour), I was at the front of the crowd directly in front of Alex Lifeson and hugging the barricade that separated audience from entertainers. In something that must be a performance rarity within the music business, the girl beside me and I actually carried on a conversation with Alex throughout the show.
Here's one such example - after Alex played a guitar solo, the girl next to me held up one hand with the international "you're number one" symbol and yelled, "You're the greatest!" Alex looked surprised, stepped back, shook his head, pointed to himself between chords, and mouthed the words, "Me? No - I don't think so..."
The next song was Limelight, which contains one of my favorite guitar solos. As Alex nailed the final notes of the solo, he looked to me and shrugged his shoulders as if to ask, "How was that?" I held up a hand with the international "OK" symbol, and I yelled, "That was pretty good!" Alex smiled and nodded, and then he replied, "Okay, I can accept that."
And that was how the rest of the show went - the anonymous girl and I commented on every song or solo, and Alex kept us entertained by his reactions. But the over-arching thing that I realized during that concert was: Alex was just a normal guy.
Despite being one of the central figures in one of the most-talented rock groups in history; Alex wasn't putting on airs, and he wasn't acting like a big rock star. Instead, he was down-playing our compliments, and playfully joking with audience members. I think that's one of the things that has endeared the members of Rush to their fans over the years: despite having earned a host of accolades, they seem indifferent and almost embarrassed by praise.
Humility in greatness - that's such a rare thing in today's self-absorbed entertainment industry, and one more reason why Rush is one of the greatest rock bands in history.
22 December 2013 • by Bob • Projects
My wife and I moved back to Arizona this past summer, and fireplaces are pretty scare here because it's generally warm for most of the winter. For example, this is the week of Christmas, and the temperatures are still in the high-60s Fahrenheit. Even though we're well into December and the rest of the country is contending with winter snow, I'm walking around in shorts and a Hawaiian shirt. So it's pretty easy to see why a fireplace isn't a selling feature for most homes in Arizona; in fact, it's often more of a nuisance.
But that being said, a lack of a fireplace has a few drawbacks during the Christmas season - and a primary downside was voiced by my wife when she recently asked, "Where are we going to hang our Christmas stockings this year?"
I have to admit - I hadn't given much thought to that question before, and I certainly wouldn't have bought a house with a fireplace just to have a place to hang stockings for a few weeks each year. Just the same, I started thinking about a way to rectify our miniscule first-world problem. My wife had travel plans which would have her out of the house for the first weekend in December, and that provided me with ample opportunity to hatch a scheme where I could do something about our stocking situation and surprise my spouse.
In case you were wondering, you read that correctly - my wife left me alone for the weekend and I spent it building a faux fireplace for her. Some guys would relish the opportunity to watch non-stop sports or action movies without having to fight for the remote, while other guys would head off to the mountains for some quality fishing or hunting. But as for me - I chose to spend my weekend wandering the aisles of my local Home Depot, Lowes, and Ace Hardware stores while picking out various parts to build something that was kind of geeky. Yup, that's just the kind of guy I am - deal with it.
First of all - why not? It's fun to build something every once in a while. ;-]
That being said, there are many different kinds of artificial fireplaces that are available on the market, but none of those met the requirements that I wanted to address:
In the end, building my own fireplace seemed to make the most sense to me. And if I ever decide that I'm done with it, I'm sure that I can donate to some high school's Drama Department. ;-]
Once I had decided to build my own fireplace, the first thing that I had to do was get some fake fireplace logs. There are dozens of variations available, so that step was pretty easy - I just had to pick a set that seemed reasonable. Once I had purchased the fake logs for the fireplace, that gave me the dimensions that I would need to build the fireplace.
The next thing that I needed to do was to come up with a construction plan - which I didn't actually do. I had a general idea of what I wanted the end result to be, but it certainly wasn't a fully-formed proposal. I figured that I would wander into my local Home Depot or Lowes and wander aimless through the aisles until something a little more solid popped into my head, and that's pretty much what I did - with one exception: I called my dad and asked if he wanted to come along for what was undoubtedly going to be a weird construction project, and he agreed. ;-]
When we arrived at the hardware store, we headed to the lumber section, where I proceeded to explain the general concept that I had been pondering for the past few days. Between the two of us, we looked at all of our options based on the wood that was available, and we came up with a design that somewhat resembled what I had been thinking - albeit with some cool revisions.
The general design that we came up with was to create four rectangular boxes that stacked to create a square frame:
Since we were basically making this up as we went along, there were a few "aha" moments where we realized that our plan wasn't going to work for some reason or other, so we changed the design several times during construction. One of the great ideas that my dad came up with was to use a single 48-inch x 48-inch x 0.5-inch board as the backing for the entire project, and that worked out great; it gave support to the whole structure, and it allowed me to use textured paint it so that it looked the inside of a fireplace.
The construction was pretty straightforward - my dad and I hauled all of the raw materials to his house, where we measured the wood and we used his table saw to cut everything to size. We started by creating the base, and once we had that built, we created the left and right sides, and we followed that by creating the crown. After we put the boxes together to see what the general idea would look like, we came up with the idea of adding the facade around the crown to give it a more finished look, and we decided to add a single piece of horizontal wood as mantle, which would extend beyond the edges of the crown. Once we had all of the boxes created, I took all of the parts home where I painted everything before the final construction.
With that in mind, here are a few photographs from the latter part of the construction process:
These first few images are obviously from the painting process, and if you look closely you'll notice my ingenious use of my lead-pellet scuba diving weights to hold down the tarp. (I have no other use for those scuba diving weights since I can no longer bring those on dive trips due to TSA-induced weight restrictions, but that's another story for another day.)
This next photo is a close-up of the front and back views for the left and right vertical boxes.
These next two photos are before-and-after shots of the partially-constructed fireplace, as seen from the back. After I had the pieces stacked correctly, I drilled holes between the different parts and secured them together with lag bolts and wing nuts.
I used several two-by-fours in this construction because the wood for the siding was fairly thin, so using two-by-fours inside the pieces allowed me to drill deeper holes and use 1.5-inch wood screws to bolt together the pieces. That being said, remember that a two-by-four is actually a 3.5-inch by 1.5-inch piece of wood, so you need to take that into account when you are measuring for wood projects. ;-]
I could have bought an actual fireplace grate upon which to rest the fake fireplace logs, but those are usually made from wrought iron because they need to stand up to the heat of an actual fire. Using a real grate would also add a bunch of unnecessary weight to the overall project, and storing the grate would be a pain. With that in mind, I decided to create my own out of PVC pipe because it would be considerably lighter, and it afforded me the option to disassemble grate for storage later.
I chose to use 1.5-inch PVC pipe to construct the grate, and here is the list of parts for that part of the project:
It took me a long time to cut all of the PVC pipe, and I had enough materials for me to cut a few different lengths for the legs in order to see what the grate looked like at a few different heights. In the end I decided on a 4-inch height for the legs - this seemed to look the best to me. Once I had everything cut, I assembled it just to make sure that everything was going to fit together, then I disassembled it and used the PVC cement to secure the parts of the construction that I didn't want slipping over time (like the legs). Once I reassembled the grate and painted it, I could still disassemble several parts of the grate if I wanted to do so, but I'll probably store the grate intact just to keep the parts together.
Despite having worked the entire construction weekend, I still wasn't quite done when Kathleen was due home, so I assembled what I had without bolting everything together. (I still had some final painting to do, and I had some decorative trim that I was still considering for the project.) Just the same, it was far enough along that I could put all of the pieces together and surprise Kathleen when she arrived. She unwittingly gave me a great compliment when asked where I had bought the fireplace. ;-]
Still, I had some work left to do and some changes that I wanted to make - so after leaving the fireplace set up for a week or so, I disassembled it, changed out some of the wood, repainted everything with several additional coats of paint, and I reassembled it.
Here's what the completed project looks like:
This was a great project to build, and it's always fun to work on a project with my dad. But the most important result was - of course - that Kathleen now has a place to hang her Christmas stockings. ;-]
20 November 2013 • by Bob • Classic ASP, IIS, SEO, URL Rewrite
Last year I wrote a blog titled Using Classic ASP and URL Rewrite for Dynamic SEO Functionality, in which I described how you could combine Classic ASP and the URL Rewrite module for IIS to dynamically create Robots.txt and Sitemap.xml files for your website, thereby helping with your Search Engine Optimization (SEO) results. A few weeks ago I had a follow-up question which I thought was worth answering in a blog post.
Here is the question that I was asked:
"What if I don't want to include all dynamic pages in sitemap.xml but only a select few or some in certain directories because I don't want bots to crawl all of them. What can I do?"
That's a great question, and it wasn't tremendously difficult for me to update my original code samples to address this request. First of all, the majority of the code from my last blog will remain unchanged - here's the file by file breakdown for the changes that need made:
Filename | Changes |
---|---|
Robots.asp | None |
Sitemap.asp | See the sample later in this blog |
Web.config | None |
So if you are already using the files from my original blog, no changes need to be made to your Robot.asp file or the URL Rewrite rules in your Web.config file because the question only concerns the files that are returned in the the output for Sitemap.xml.
The good news it, I wrote most of the heavy duty code in my last blog - there were only a few changes that needed to made in order to accommodate the requested functionality. The main difference is that the original Sitemap.asp file used to have a section that recursively parsed the entire website and listed all of the files in the website, whereas this new version moves that section of code into a separate function to which you pass the unique folder name to parse recursively. This allows you to specify only those folders within your website that you want in the resultant sitemap output.
With that being said, here's the new code for the Sitemap.asp file:
<% Option Explicit On Error Resume Next Response.Clear Response.Buffer = True Response.AddHeader "Connection", "Keep-Alive" Response.CacheControl = "public" Dim strUrlRoot, strPhysicalRoot, strFormat Dim objFSO, objFolder, objFile strPhysicalRoot = Server.MapPath("/") Set objFSO = Server.CreateObject("Scripting.Filesystemobject") strUrlRoot = "http://" & Request.ServerVariables("HTTP_HOST") ' Check for XML or TXT format. If UCase(Trim(Request("format")))="XML" Then strFormat = "XML" Response.ContentType = "text/xml" Else strFormat = "TXT" Response.ContentType = "text/plain" End If ' Add the UTF-8 Byte Order Mark. Response.Write Chr(CByte("&hEF")) Response.Write Chr(CByte("&hBB")) Response.Write Chr(CByte("&hBF")) If strFormat = "XML" Then Response.Write "<?xml version=""1.0"" encoding=""UTF-8""?>" & vbCrLf Response.Write "<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">" & vbCrLf End if ' Always output the root of the website. Call WriteUrl(strUrlRoot,Now,"weekly",strFormat) ' Output only specific folders. Call ParseFolder("/marketing") Call ParseFolder("/sales") Call ParseFolder("/hr/jobs") ' -------------------------------------------------- ' End of file system loop. ' -------------------------------------------------- If strFormat = "XML" Then Response.Write "</urlset>" End If Response.End ' ====================================================================== ' ' Recursively walks a folder path and return URLs based on the ' static *.html files that it locates. ' ' strRootFolder = The base path for recursion ' ' ====================================================================== Sub ParseFolder(strParentFolder) On Error Resume Next Dim strChildFolders, lngChildFolders Dim strUrlRelative, strExt ' Get the list of child folders under a parent folder. strChildFolders = GetFolderTree(Server.MapPath(strParentFolder)) ' Loop through the collection of folders. For lngChildFolders = 1 to UBound(strChildFolders) strUrlRelative = Replace(Mid(strChildFolders(lngChildFolders),Len(strPhysicalRoot)+1),"\","/") Set objFolder = objFSO.GetFolder(Server.MapPath("." & strUrlRelative)) ' Loop through the collection of files. For Each objFile in objFolder.Files strExt = objFSO.GetExtensionName(objFile.Name) If StrComp(strExt,"html",vbTextCompare)=0 Then If StrComp(Left(objFile.Name,6),"google",vbTextCompare)<>0 Then Call WriteUrl(strUrlRoot & strUrlRelative & "/" & objFile.Name, objFile.DateLastModified, "weekly", strFormat) End If End If Next Next End Sub ' ====================================================================== ' ' Outputs a sitemap URL to the client in XML or TXT format. ' ' tmpStrFreq = always|hourly|daily|weekly|monthly|yearly|never ' tmpStrFormat = TXT|XML ' ' ====================================================================== Sub WriteUrl(tmpStrUrl,tmpLastModified,tmpStrFreq,tmpStrFormat) On Error Resume Next Dim tmpDate : tmpDate = CDate(tmpLastModified) ' Check if the request is for XML or TXT and return the appropriate syntax. If tmpStrFormat = "XML" Then Response.Write " <url>" & vbCrLf Response.Write " <loc>" & Server.HtmlEncode(tmpStrUrl) & "</loc>" & vbCrLf Response.Write " <lastmod>" & Year(tmpLastModified) & "-" & Right("0" & Month(tmpLastModified),2) & "-" & Right("0" & Day(tmpLastModified),2) & "</lastmod>" & vbCrLf Response.Write " <changefreq>" & tmpStrFreq & "</changefreq>" & vbCrLf Response.Write " </url>" & vbCrLf Else Response.Write tmpStrUrl & vbCrLf End If End Sub ' ====================================================================== ' ' Returns a string array of folders under a root path ' ' ====================================================================== Function GetFolderTree(strBaseFolder) Dim tmpFolderCount,tmpBaseCount Dim tmpFolders() Dim tmpFSO,tmpFolder,tmpSubFolder ' Define the initial values for the folder counters. tmpFolderCount = 1 tmpBaseCount = 0 ' Dimension an array to hold the folder names. ReDim tmpFolders(1) ' Store the root folder in the array. tmpFolders(tmpFolderCount) = strBaseFolder ' Create file system object. Set tmpFSO = Server.CreateObject("Scripting.Filesystemobject") ' Loop while we still have folders to process. While tmpFolderCount <> tmpBaseCount ' Set up a folder object to a base folder. Set tmpFolder = tmpFSO.GetFolder(tmpFolders(tmpBaseCount+1)) ' Loop through the collection of subfolders for the base folder. For Each tmpSubFolder In tmpFolder.SubFolders ' Increment the folder count. tmpFolderCount = tmpFolderCount + 1 ' Increase the array size ReDim Preserve tmpFolders(tmpFolderCount) ' Store the folder name in the array. tmpFolders(tmpFolderCount) = tmpSubFolder.Path Next ' Increment the base folder counter. tmpBaseCount = tmpBaseCount + 1 Wend GetFolderTree = tmpFolders End Function %>
It should be easily seen that the code is largely unchanged from my previous blog.
One last thing to consider, I didn't make any changes to the Robots.asp file in this blog. But that being said, when you do not want specific paths crawled, you should add rules to your Robots.txt file to disallow those paths. For example, here is a simple Robots.txt file which allows your entire website:
# Robots.txt # For more information on this file see: # http://www.robotstxt.org/ # Define the sitemap path Sitemap: http://localhost:53644/sitemap.xml # Make changes for all web spiders User-agent: * Allow: / Disallow:
If you were going to deny crawling on certain paths, you would need to add the specific paths that you do not want crawled to your Robots.txt file like the following example:
# Robots.txt # For more information on this file see: # http://www.robotstxt.org/ # Define the sitemap path Sitemap: http://localhost:53644/sitemap.xml # Make changes for all web spiders User-agent: * Disallow: /foo Disallow: /bar
With that being said, if you are using my Robots.asp file from my last blog, you would need to update the section of code that defines the paths like my previous example:
Response.Write "# Make changes for all web spiders" & vbCrLf Response.Write "User-agent: *" & vbCrLf Response.Write "Disallow: /foo" & vbCrLf Response.Write "Disallow: /bar" & vbCrLf
I hope this helps. ;-]
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
15 November 2013 • by Bob • SSL, IIS Express, WebMatrix, Visual Studio, Internet Explorer
I had an interesting question from a coworker today that I thought would make a great blog. Here's the scenario...
My coworker was using WebMatrix to create a website, although he could have been using Visual Studio and he would have run into the same problem. The problem he was seeing was that his application required HTTPS, but he was greeted with the following error message every time that he used Internet Explorer to browse to his development website at https://localhost:44300/:
When he clicked the link to Continue to this website, he could click on Certificate error in the address bar, which would inform him that the website was using an Untrusted certificate:
If he clicked View certificates, the Certificate dialog box informed him that the CA Root certificate was not trusted:
Since my coworker was using WebMatrix with IIS Express, which is the default development web server for WebMatrix and Visual Studio, all HTTPS communication was using the self-signed certificate from IIS Express. Since that certificate is self-signed, it is not trusted as if it was issued from a "Trusted Root Certification Authority," and therefore Internet Explorer (or any other security-conscious web browser) was doing the right thing by warning the end-user that they were using an untrusted certificate for HTTPS.
If you were seeing this error when browsing to an Internet website, this would be "A Very Bad Thing™", because you might be sending your confidential information to an untrusted website.
Fortunately this situation can be easily rectified, and there are two different approaches that you can use, and I will discuss both in the subsequent sections.
The easiest solution is to configure your user account to trust the self-signed certificate as though it were issued by a trusted root certificate authority. To do so, use the following steps:
A more-detailed approach is to configure your computer system to trust the IIS Express certificate, and you might want to do this if your computer is shared by several developers who log in with their individual accounts. To configure your computer to trust the IIS Express certificate, use the following steps:
Once you have completed all of the steps in one of the resolutions, you should use the following steps to test the installation of your IIS Express certificate as a trusted root certification authority:
This blog was a little longer than some of my past blogs, but it should provide you with the information you need to trust HTTPS-based websites that you are developing with IIS Express.
That wraps it up for today's blog post. ;-]
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
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/