Just a short, simple blog for Bob to share his thoughts.
24 May 2013 • by Bob • IIS, FTP
I get a lot of questions about Active versus Passive FTP Connections, specifically when people are configuring their FTP firewall settings as described in my articles like Configuring FTP Firewall Settings in IIS and FTP Firewall Support, and I get related questions when people are trying to figure out why they can't use the command-line FTP.EXE utility that ships with Windows over the Internet. With all of this in mind, I thought that I would put together a quick blog that explains Active and Passive FTP connections and why those matter when you are connecting to an FTP server over the Internet.
Here is the briefest description that I can think of to describe the different between Active and Passive FTP:
That sums up the whole concept into two bullet points. Unfortunately, neither of those bullet points are the least bit significant to you if you don't understand what they actually mean, so I will describe them in detail below.
The following output shows the communication between an FTP client and FTP server using Active FTP to retrieve a simple directory listing, and I manually color-coded the output so that the client and server's responses would be a little easier to see:
OPEN ftp.contoso.com Resolving ftp.contoso.com... Connect socket #1920 to 169.254.10.1, port 21... 220 Microsoft FTP Service HOST ftp.contoso.com 220 Host accepted. USER robert 331 Password required for robert. PASS ********** 230 User logged in. PWD 257 "/" is current directory. PORT 169,254,10,2,72,50 200 PORT command successful. LIST 125 Data connection already open; Transfer starting. drwxrwxrwx 1 owner group 0 Feb 15 19:26 aspnet_client -rwxrwxrwx 1 owner group 689 Jan 31 22:27 default.htm 226 Transfer complete. Transferred 106 bytes in 0.008 seconds QUIT 221 Goodbye.
In the beginning of this exchange, the client connects to the server from one of its ephemeral data ports to the server's port for the FTP command channel. After negotiating the FTP host, username, and password, the client retrieves the name of the current directory. So far all of the client/server communication has taken place over the FTP command channel, and up to now the conversation is identical to Passive FTP, but that is about to change.
The client's next task is to request a directory listing, which is denoted by the LIST
command. The server will return its response to the client over the data channel, so before FTP client can send the LIST
command, the client has to specify whether to use Active or Passive. In this example, the client has specified Active FTP by sending a PORT
command. The syntax for this command is PORT A1,A2,A3,A4,P1,P2
, where A1
through A4
are octets of the client's IPv4 address, and P1
/P2
are two bytes that make up a 16-bit (0-65535) port address on the client. (Note: if you are using IPv6, there is a similar EPRT
command that works with IPv6 addresses.)
Here's what the information in the PORT
command means: the FTP client is essentially telling the FTP server, "For the upcoming data transfer, you need to talk to me at this IP address on this port." This means that the FTP client is actively in control of how the subsequent data communication is going to take place.
If we analyze this information, you can easily see why Active FTP will often fail to work over the Internet. As a relevant example, if you were to use the FTP.EXE client that ships with Windows, it can only use Active FTP. So when a client computer requests something from the server that needs to use the data channel, the client computer sends its IP address via a PORT
command. If the FTP client is behind a firewall or NAT server, then the client is going to send its internal, LAN-based address, to which the FTP server will more than likely fail to connect. For example, if you are on a LAN that uses a NAT server and you have a 192.168.0.nnn IPv4 address, that IP address is invalid over the Internet, so the server will never be able to establish a data connection to your client to send the data. (Note: This is the reason why many customers contact me with the following problem description: "I can use FTP.EXE to connect to my server, and everything works until I try to retrieve a directory listing, then it hangs until the connection times out." What is actually happening is the FTP server is trying to connect to the FTP client's IP address and port that were specified by the PORT command, but the connection does not succeed because the server cannot connect to the private IP address of the client.)
The following output shows the communication between an FTP client and FTP server using Passive FTP to retrieve the same directory listing as my previous example, and once again I manually color-coded the output so that the client and server's responses would be a little easier to see:
OPEN ftp.contoso.com Resolving ftp.contoso.com... Connect socket #2076 to 169.254.10.1, port 21... 220 Microsoft FTP Service HOST ftp.contoso.com 220 Host accepted. USER robert 331 Password required for robert. PASS ********** 230 User logged in. PWD 257 "/" is current directory. PASV 227 Entering Passive Mode (169,254,10,1,197,19). LIST Connect socket #2104 to 169.254.10.1, port 50451... 150 Opening ASCII mode data connection. drwxrwxrwx 1 owner group 0 Feb 15 19:26 aspnet_client -rwxrwxrwx 1 owner group 689 Jan 31 22:27 default.htm 226 Transfer complete. Transferred 106 bytes in 0.008 seconds QUIT 221 Goodbye.
As I mentioned in my earlier example, the beginning of this conversation is identical to Active FTP: the client connects from one of its ephemeral data ports to the server's port for the FTP command channel. After negotiating the FTP host, username, and password, the client retrieves the name of the current directory as in my earlier example - and here's where the difference begins.
Once again the client's next task is to request the directory listing, which is still denoted by the LIST
command. But in this second example, the client has specified Passive FTP by sending a PASV
command. The server responds to this command with a reply that is in the format of 227 Entering Passive Mode (A1,A2,A3,A4,P1,P2)
, where A1
through A4
are octets of the server's IPv4 address, and P1
/P2
are two bytes that make up a 16-bit (0-65535) port address on the server. (Note: if you are using IPv6, there is a similar EPSV
command that works with IPv6 addresses.)
Here's what the information in the response to the PASV
command means: the FTP client is essentially telling the FTP server, "For the upcoming data transfer, you need to tell me which IP address and port I should use to talk to you." This means that the FTP client is passively allowing the server to control how the subsequent data communication is going to take place.
If we analyze this information, you can easily see why Passive FTP often works over the Internet; when the FTP server is in control of the communication parameters, it doesn't matter whether the FTP client is behind a NAT server, because the server is telling the client how it should communicate with the server.
All of this leads to an obvious question: what happens when both the server and the client are behind NAT servers or firewalls? This is where a little bit of configuration comes into play. If you read my Configuring FTP Firewall Settings in IIS and FTP Firewall Support articles, you would notice that you can configure the IIS FTP service to tell the FTP client which IP address to use; when your FTP server is located behind a firewall, you would configure your FTP server to send the external IP address of your firewall, and then you would configure your firewall to route FTP requests on that IP address to your FTP server. Since the FTP server is sending the external IP address of your firewall, the client knows how to communicate to the FTP server even though it is behind a firewall, even if your server is using a LAN-based internal IP address.
Having explained everything in my preceding examples, you should now understand what I meant earlier when I described the difference between Active and Passive FTP with these two simple points:
I hope this clears up some questions you might have about Active versus Passive FTP, why you sometimes need to configure your firewall settings for your FTP service, and why the built-in FTP client for Windows seldom works over the Internet.
That wraps it up for today's blog post. ;-]
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
30 April 2013 • by Bob • IIS, Content
Publishing technical documentation is an interesting business, and a lot of discussion & deliberation goes into the creation process for articles and videos that we produce at Microsoft. For example, when I am writing an article for IIS, should I publish that on www.iis.net, or technet.microsoft.com, or msdn.microsoft.com? Or should I just write a blog about it? And after I have published an article, how will my intended audience find it? As we continue to publish hundreds of technical articles to the websites that I just mentioned, the navigation hierarchy becomes increasingly complex, and content discoverability suffers.
Some time ago a few of our writers began to experiment with a new way to consolidate lists of related content into something that we called a "Content Map." The following pages will show you an example of what the Content Map concept looks like:
Each of these articles received a great deal of positive feedback from customers, but our team wanted to see if there was a way that customers could help us to improve on this design. We know that there is a great deal of third-party content on the Internet, and we wanted a way to recognize that. We also asked several customers about what kinds of content they need to be successful, and we added their suggestions to our deliberation process.
As a result of our collective discussions, we came up with an idea for what we are internally calling "Curated Content Views." These "views" are lists of related content topics that are organized to answer a particular question or customer need. A view is assembled by someone at Microsoft based on input from anyone who thinks that an article, blog, video, or code sample might be beneficial as part of the view.
With that in mind, here are three conceptual content views that a few of the writers on our content team have assembled:
Our team is requesting feedback from members of the community regarding these conceptual views with regard to the level of detail that is included in each view, the conceptual layouts that were used, and any thoughts about how this content compares with existing table of contents topics or content maps. You can reply to our content team via email, or you can post a response to this blog.
While we are interested in any feedback you may have, our team has put together the following list of specific questions to think about:
Thanks!
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
24 April 2013 • by Bob • IIS, WebDAV, IIS 6
A few years ago I wrote the following blog, wherein I described how the WebDAV functionality in IIS 6.0 worked with files that are Compound Document format:
IIS 6.0 WebDAV and Compound Document Format Files
As I explained in that blog post, WebDAV needs somewhere to store "properties" for files that are uploaded to the server, and WebDAV uses the compound document format to accomplish this according to the following implementation logic:
I recently had a customer contact me in order to ask if there was a way to disable this functionality since he didn't want his files modified in order to store the WebDAV properties. Unfortunately there is no built-in option for IIS that will disable this functionality, but there are a few workarounds.
First and foremost - you can change your file type to something other than the compound document format. For example, if you are uploading files that were created in Microsoft Office, if you can upload your files in the newer Office Open XML formats, then you will not run into this problem. By way of explanation, older Microsoft Office files are in compound document format, whereas files that are that are created with Microsoft Office 2010 and later are in a zipped, XML-based file format. These files will have extensions like *.DOCX for Microsoft Word documents, *.XLSX for Microsoft Excel spreadsheets, and *.PPTX for Microsoft PowerPoint presentations.
If you are using a file that must be in compound document format, like a setup package in Microsoft Installer (*.MSI) format, you can upload the file in a *.ZIP file, or you can wrap the setup package inside a self-extracting executable by using a technology like Microsoft's IExpress Wizard (which ships as a built-in utility with most versions of Windows).
If you absolutely cannot change your document from compound document format, I have a completely unsupported workaround that I can suggest. Since the problem arises when properties are added to a file, you can find a way to intercept the WebDAV commands that try to set properties. The actual HTTP verb that is used is PROPPATCH, so if you can find a way to keep this command from being used, then you can prevent files from being modified. Unfortunately you cannot simply suppress PROPPATCH commands by using a security tool like Microsoft's UrlScan to block the command, because this will cause many WebDAV clients to fail.
Instead, what I did as a workaround was to write an example ISAPI filter for IIS 6.0 that intercepts incoming PROPPATCH commands and always sends a successful (e.g. "200 OK") response to the WebDAV client, but in reality the filter does nothing with the properties and ends the request processing. This tricks a WebDAV client into thinking that it succeeded, and it prevents your files in compound document format from being modified. However, this also means that no WebDAV properties will ever be stored with your files; but if that's acceptable to you, (and it usually should be), then you can use this workaround.
With that in mind, here's the C++ code for my example ISAPI filter, and please remember that this is a completely unsupported workaround that is intended for use only when you cannot repackage your files to use something other than the compound document format.
#define _WIN32_WINNT 0x0400 #include <windows.h> #include <httpfilt.h> #define STRSAFE_LIB #include <strsafe.h> #define BUFFER_SIZE 2048 const char xmlpart1[] = "<?xml version=\"1.0\"?>" "<a:multistatus xmlns:a=\"DAV:\">" "<a:response>" "<a:href>"; const char xmlpart2[] = "</a:href>" "<a:propstat>" "<a:status>HTTP/1.1 200 OK</a:status>" "</a:propstat>" "</a:response>" "</a:multistatus>"; BOOL WINAPI GetFilterVersion(PHTTP_FILTER_VERSION pVer) { HRESULT hr = S_OK; // Set the filter's version. pVer->dwFilterVersion = HTTP_FILTER_REVISION; // Set the filter's description. hr = StringCchCopyEx( pVer->lpszFilterDesc,256,"PROPPATCH", NULL,NULL,STRSAFE_IGNORE_NULLS); if (FAILED(hr)) return FALSE; // Set the filter's flags. pVer->dwFlags = SF_NOTIFY_ORDER_HIGH | SF_NOTIFY_PREPROC_HEADERS; return TRUE; } DWORD WINAPI HttpFilterProc( PHTTP_FILTER_CONTEXT pfc, DWORD NotificationType, LPVOID pvNotification ) { // Verify the correct notification. if ( NotificationType == SF_NOTIFY_PREPROC_HEADERS) { PHTTP_FILTER_PREPROC_HEADERS pHeaders; HRESULT hr = S_OK; bool fSecure = false; char szServerName[BUFFER_SIZE] = ""; char szSecure[2] = ""; char szResponseXML[BUFFER_SIZE] = ""; char szResponseURL[BUFFER_SIZE] = ""; char szRequestURL[BUFFER_SIZE] = ""; char szMethod[BUFFER_SIZE] = ""; DWORD dwBuffSize = 0; pHeaders = (PHTTP_FILTER_PREPROC_HEADERS) pvNotification; // Get the method of the request dwBuffSize = BUFFER_SIZE-1; // Exit with an error status if a failure occured. if (!pfc->GetServerVariable( pfc, "HTTP_METHOD", szMethod, &dwBuffSize)) return SF_STATUS_REQ_ERROR; if (strcmp(szMethod, "PROPPATCH") == 0) { // Send the HTTP status to the client. if (!pfc->ServerSupportFunction( pfc, SF_REQ_SEND_RESPONSE_HEADER,"207 Multi-Status", 0, 0)) return SF_STATUS_REQ_ERROR; // Get the URL of the request. dwBuffSize = BUFFER_SIZE-1; if (!pfc->GetServerVariable( pfc, "URL", szRequestURL, &dwBuffSize)) return SF_STATUS_REQ_ERROR; // Determine if request was sent over secure port. dwBuffSize = 2; if (!pfc->GetServerVariable( pfc, "SERVER_PORT_SECURE", szSecure, &dwBuffSize)) return SF_STATUS_REQ_ERROR; fSecure = (szSecure[0] == '1'); // Get the server name. dwBuffSize = BUFFER_SIZE-1; if (!pfc->GetServerVariable( pfc, "SERVER_NAME", szServerName, &dwBuffSize)) return SF_STATUS_REQ_ERROR; // Set the response URL. hr = StringCchPrintf( szResponseURL,BUFFER_SIZE-1, "http%s://%s/%s", (fSecure ? "s" : ""), szServerName, &szRequestURL[1]); // Exit with an error status if a failure occurs. if (FAILED(hr)) return SF_STATUS_REQ_ERROR; // Set the response body. hr = StringCchPrintf( szResponseXML,BUFFER_SIZE-1, "%s%s%s", xmlpart1, szResponseURL, xmlpart2); // Exit with an error status if a failure occurs. if (FAILED(hr)) return SF_STATUS_REQ_ERROR; // Write the response body to the client. dwBuffSize = strlen(szResponseXML); if (!pfc->WriteClient( pfc, szResponseXML, &dwBuffSize, 0)) return SF_STATUS_REQ_ERROR; // Flag the request as completed. return SF_STATUS_REQ_FINISHED; } } return SF_STATUS_REQ_NEXT_NOTIFICATION; }
I hope this helps. ;-]
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
28 March 2013 • by Bob • IIS, Scripting, FTP
A customer asked me a question a little while ago that provided me the opportunity to recycle some code that I had written many years ago. In so doing, I also made a bunch of updates to the code to make it considerably more useful, and I thought that it would make a great blog.
Here's the scenario: a customer had hundreds of user accounts created, and he wanted to use the FTP service's User Isolation features to restrict each user to a specific folder on his FTP site. Since it would take a long time to manually create a folder for each user account, the customer wanted to know if there was a way to automate the process. As it turns out, I had posted a very simple script in the IIS.net forums several years ago that did something like what he wanted; and that script was based off an earlier script that I had written for someone else back in the IIS 6.0 days.
One quick reminder - FTP User Isolation uses a specific set of folders for user accounts, which are listed in the table below.
User Account Types | Home Directory Syntax |
---|---|
Anonymous users | %FtpRoot%\LocalUser\Public |
Local Windows user accounts
(Requires Basic authentication.) |
%FtpRoot%\LocalUser\%UserName% |
Windows domain accounts
(Requires Basic authentication.) |
%FtpRoot%\%UserDomain%\%UserName% |
Note: %FtpRoot% is the root directory for your FTP site: for example, C:\Inetpub\Ftproot.
That being said, I'm a big believer in recycling code, so I found the last version of that script that I gave to someone and I made a bunch of changes to it so it would be more useful for the customer. What that in mind, here's the resulting script, and I'll explain a little more about what it does after the code sample.
Option Explicit ' Define the root path for the user isolation folders. ' This should be the root directory for your FTP site. Dim strRootPath : strRootPath = "C:\Inetpub\wwwroot\" ' Define the name of the domain or the computer to use. ' Leave this blank for the local computer. Dim strComputerOrDomain : strComputerOrDomain = "" ' Define the remaining script variables. Dim objFSO, objCollection, objUser, objNetwork, strContainerName ' Create a network object; used to query the computer name. Set objNetwork = WScript.CreateObject("WScript.Network") ' Create a file system object; used to creat folders. Set objFSO = CreateObject("Scripting.FileSystemObject") ' Test if the computer name is null. If Len(strComputerOrDomain)=0 Or strComputerOrDomain="." Then ' If so, define the local computer name as the account repository. strComputerOrDomain = objNetwork.ComputerName End If ' Verify that the root path exists. If objFSO.FolderExists(strRootPath) Then ' Test if the script is using local users. If StrComp(strComputerOrDomain,objNetwork.ComputerName,vbTextCompare)=0 Then ' If so, define the local users container path. strContainerName = "LocalUser" ' And define the users collection as local. Set objCollection = GetObject("WinNT://.") Else ' Otherwise, use the source name as the path. strContainerName = strComputerOrDomain ' And define the users collection as remote. Set objCollection = GetObject("WinNT://" & strComputerOrDomain & "") End If ' Append trailing backslash if necessary. If Right(strRootPath,1)<>"\" Then strRootPath = strRootPath & "\" ' Define the adjusted root path for the container folder. strRootPath = strRootPath & strContainerName & "\" ' Test if the container folder already exists. If objFSO.FolderExists(strRootPath)=False Then ' Create the container folder if necessary. objFSO.CreateFolder(strRootPath) End If ' Specify the collection filter for user objects only. objCollection.Filter = Array("user") ' Loop through the users collection. For Each objUser In objCollection ' Test if the user's account is enabled. If objUser.AccountDisabled = False Then ' Test if the user's folder already exists. If objFSO.FolderExists(strRootPath & "\" & objUser.Name)=False Then ' Create the user's folder if necessary. objFSO.CreateFolder(strRootPath & "\" & objUser.Name) End If End If Next End If
I documented this script in great detail, so it should be self-explanatory for the most part. But just to be on the safe side, here's an explanation of what this script is doing when you run it on your FTP server:
That's all for now. ;-]
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
31 December 2012 • by Bob • IIS, URL Rewrite, SEO, Classic ASP
I had another interesting situation present itself recently that I thought would make a good blog: how to use Classic ASP with the IIS URL Rewrite module to dynamically generate Robots.txt and Sitemap.xml files.
Here's the situation: I host a website for one of my family members, and like everyone else on the Internet, he wanted some better SEO rankings. We discussed a few things that he could do to improve his visibility with search engines, and one of the suggestions that I gave him was to keep his Robots.txt and Sitemap.xml files up-to-date. But there was an additional caveat - he uses two separate DNS names for the same website, and that presents a problem for absolute URLs in either of those files. Before anyone points out that it's usually not a good idea to host multiple DNS names on the same content, there are times when this is acceptable; for example, if you are trying to decide which of several DNS names is the best to use, you might want to bind each name to the same IP address and parse your logs to find out which address is getting the most traffic.
In any event, the syntax for both Robots.txt and Sitemap.xml files is pretty easy, so I wrote a couple of simple Classic ASP Robots.asp and Sitemap.asp pages that output the correct syntax and DNS-specific URLs for each domain name, and I wrote some simple URL Rewrite rules that rewrite inbound requests for Robots.txt and Sitemap.xml files to the ASP pages, while blocking direct access to the Classic ASP pages themselves.
All of that being said, there are a couple of quick things that I would like to mention before I get to the code:
That being said, let's move on to the actual code.
There are three files that you will need to create for this example:
You need to save the following code sample as Robots.asp in the root of your website; this page will be executed whenever someone requests the Robots.txt file for your website. This example is very simple: it checks for the requested hostname and uses that to dynamically create the absolute URL for the website's Sitemap.xml file.
<% Option Explicit On Error Resume Next Dim strUrlRoot Dim strHttpHost Dim strUserAgent Response.Clear Response.Buffer = True Response.ContentType = "text/plain" Response.CacheControl = "public" Response.Write "# Robots.txt" & vbCrLf Response.Write "# For more information on this file see:" & vbCrLf Response.Write "# http://www.robotstxt.org/" & vbCrLf & vbCrLf strHttpHost = LCase(Request.ServerVariables("HTTP_HOST")) strUserAgent = LCase(Request.ServerVariables("HTTP_USER_AGENT")) strUrlRoot = "http://" & strHttpHost Response.Write "# Define the sitemap path" & vbCrLf Response.Write "Sitemap: " & strUrlRoot & "/sitemap.xml" & vbCrLf & vbCrLf Response.Write "# Make changes for all web spiders" & vbCrLf Response.Write "User-agent: *" & vbCrLf Response.Write "Allow: /" & vbCrLf Response.Write "Disallow: " & vbCrLf Response.End %>
The following example file is also pretty simple, and you would save this code as Sitemap.asp in the root of your website. There is a section in the code where it loops through the file system looking for files with the *.html file extension and only creates URLs for those files. If you want other files included in your results, or you want to change the code from static to dynamic content, this is where you would need to update the file accordingly.
<% Option Explicit On Error Resume Next Response.Clear Response.Buffer = True Response.AddHeader "Connection", "Keep-Alive" Response.CacheControl = "public" Dim strFolderArray, lngFolderArray Dim strUrlRoot, strPhysicalRoot, strFormat Dim strUrlRelative, strExt 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) ' -------------------------------------------------- ' This following section contains the logic to parse ' the directory tree and return URLs based on the ' static *.html files that it locates. This is where ' you would change the code for dynamic content. ' -------------------------------------------------- strFolderArray = GetFolderTree(strPhysicalRoot) For lngFolderArray = 1 to UBound(strFolderArray) strUrlRelative = Replace(Mid(strFolderArray(lngFolderArray),Len(strPhysicalRoot)+1),"\","/") Set objFolder = objFSO.GetFolder(Server.MapPath("." & strUrlRelative)) 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 of file system loop. ' -------------------------------------------------- If strFormat = "XML" Then Response.Write "</urlset>" End If Response.End ' ====================================================================== ' ' 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 %>
Note: There are two helper methods in the preceding example that I should call out:
The last step is to add the URL Rewrite rules to the Web.config file in the root of your website. The following example is a complete Web.config file, but you could merge the rules into your existing Web.config file if you have already created one for your website. These rules are pretty simple, they rewrite all inbound requests for Robots.txt to Robots.asp, and they rewrite all requests for Sitemap.xml to Sitemap.asp?format=XML and requests for Sitemap.txt to Sitemap.asp?format=TXT; this allows requests for both the XML-based and text-based sitemaps to work, even though the Robots.txt file contains the path to the XML file. The last part of the URL Rewrite syntax returns HTTP 404 errors if anyone tries to send direct requests for either the Robots.asp or Sitemap.asp files; this isn't absolutely necesary, but I like to mask what I'm doing from prying eyes. (I'm kind of geeky that way.)
<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <rewrite> <rewriteMaps> <clear /> <rewriteMap name="Static URL Rewrites"> <add key="/robots.txt" value="/robots.asp" /> <add key="/sitemap.xml" value="/sitemap.asp?format=XML" /> <add key="/sitemap.txt" value="/sitemap.asp?format=TXT" /> </rewriteMap> <rewriteMap name="Static URL Failures"> <add key="/robots.asp" value="/" /> <add key="/sitemap.asp" value="/" /> </rewriteMap> </rewriteMaps> <rules> <clear /> <rule name="Static URL Rewrites" patternSyntax="ECMAScript" stopProcessing="true"> <match url=".*" ignoreCase="true" negate="false" /> <conditions> <add input="{Static URL Rewrites:{REQUEST_URI}}" pattern="(.+)" /> </conditions> <action type="Rewrite" url="{C:1}" appendQueryString="false" redirectType="Temporary" /> </rule> <rule name="Static URL Failures" patternSyntax="ECMAScript" stopProcessing="true"> <match url=".*" ignoreCase="true" negate="false" /> <conditions> <add input="{Static URL Failures:{REQUEST_URI}}" pattern="(.+)" /> </conditions> <action type="CustomResponse" statusCode="404" subStatusCode="0" /> </rule> <rule name="Prevent rewriting for static files" patternSyntax="Wildcard" stopProcessing="true"> <match url="*" /> <conditions> <add input="{REQUEST_FILENAME}" matchType="IsFile" /> </conditions> <action type="None" /> </rule> </rules> </rewrite> </system.webServer> </configuration>
That sums it up for this blog; I hope that you get some good ideas from it.
For more information about the syntax in Robots.txt and Sitemap.xml files, see the following URLs:
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
03 October 2012 • by Bob • IIS, Scripting, FTP, Extensibility, IIS, Scripting, FTP, Extensibility
I was recently contacted by someone who was trying to use Windows Management Instrumentation (WMI) code to stop and restart FTP websites by using code that he had written for IIS 6.0; his code was something similar to the following:
Option Explicit On Error Resume Next Dim objWMIService, colItems, objItem ' Attach to the IIS service. Set objWMIService = GetObject("winmgmts:\root\microsoftiisv2") ' Retrieve the collection of FTP sites. Set colItems = objWMIService.ExecQuery("Select * from IIsFtpServer") ' Loop through the sites collection. For Each objItem in colItems ' Restart one single website. If (objItem.Name = "MSFTPSVC/1") Then Err.Clear objItem.Stop If (Err.Number <> 0) Then WScript.Echo Err.Number objItem.Start If (Err.Number <> 0) Then WScript.Echo Err.Number End If Next
The problem that the customer was seeing is that this query did not return the list of FTP-based websites for IIS 7.0 or IIS 7.5 (called IIS7 henceforth), although changing the class in the query from IIsFtpServer to IIsWebServer would make the script work with HTTP-based websites those versions of IIS7.
The problem with the customer's code was that he is using WMI to manage IIS7; this relies on our old management APIs that have been deprecated, although part of that model is partially available through the metabase compatibility feature in IIS7. Here's what I mean by "partially": only a portion of the old ADSI/WMI objects are available, and unfortunately FTP is not part of the objects that can be scripted through the metabase compatibility feature in IIS7.
That being said, what the customer wants to do is still possible through scripting in both IIS7 and IIS8, and the following sample shows how to loop through all of the sites, determine which sites have FTP bindings, and then stop/start FTP for each site. To use this script, copy the code into a text editor like Windows Notepad and save it with a name like "RestartAllFtpSites.vbs" to your system, then double-click the file to run it.
' Temporarily disable breaking on runtime errors. On Error Resume Next ' Create an Admin Manager object. Set adminManager = CreateObject("Microsoft.ApplicationHost.AdminManager") adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST" ' Test for commit path support. If Err.Number <> 0 Then Err.Clear ' Create a Writable Admin Manager object. Set adminManager = CreateObject("Microsoft.ApplicationHost.WritableAdminManager") adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST" If Err.Number <> 0 Then WScript.Quit End If ' Resume breaking on runtime errors. On Error Goto 0 ' Retrieve the sites collection. Set sitesSection = adminManager.GetAdminSection("system.applicationHost/sites", "MACHINE/WEBROOT/APPHOST") Set sitesCollection = sitesSection.Collection ' Loop through the sites collection. For siteCount = 0 To CInt(sitesCollection.Count)-1 isFtpSite = False ' Determine if the current site is an FTP site by checking the bindings. Set siteElement = sitesCollection(siteCount) Set bindingsCollection = siteElement.ChildElements.Item("bindings").Collection For bindingsCount = 0 To CInt(bindingsCollection.Count)-1 Set bindingElement = bindingsCollection(bindingsCount) If StrComp(CStr(bindingElement.Properties.Item("protocol").Value),"ftp",vbTextCompare)=0 Then isFtpSite = True Exit For End If Next ' If it's an FTP site, start and stop the site. If isFtpSite = True Then Set ftpServerElement = siteElement.ChildElements.Item("ftpServer") ' Create an instance of the Stop method. Set stopFtpSite = ftpServerElement.Methods.Item("Stop").CreateInstance() ' Execute the method to stop the FTP site. stopFtpSite.Execute() ' Create an instance of the Start method. Set startFtpSite = ftpServerElement.Methods.Item("Start").CreateInstance() ' Execute the method to start the FTP site. startFtpSite.Execute() End If Next
And the following code sample shows how to stop/start a single FTP site. To use this script, copy the code into a text editor like Windows Notepad, rename the site name appropriately for one of your FTP sites, save it with a name like "RestartContosoFtpSite.vbs" to your system, then double-click the file to run it.
' Temporarily disable breaking on runtime errors. On Error Resume Next ' Create an Admin Manager object. Set adminManager = CreateObject("Microsoft.ApplicationHost.AdminManager") adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST" ' Test for commit path support. If Err.Number <> 0 Then Err.Clear ' Create a Writable Admin Manager object. Set adminManager = CreateObject("Microsoft.ApplicationHost.WritableAdminManager") adminManager.CommitPath = "MACHINE/WEBROOT/APPHOST" If Err.Number <> 0 Then WScript.Quit End If ' Resume breaking on runtime errors. On Error Goto 0 ' Retrieve the sites collection. Set sitesSection = adminManager.GetAdminSection("system.applicationHost/sites", "MACHINE/WEBROOT/APPHOST") Set sitesCollection = sitesSection.Collection ' Locate a specific site. siteElementPos = FindElement(sitesCollection, "site", Array("name", "ftp.contoso.com")) If siteElementPos = -1 Then WScript.Echo "Site was not found!" WScript.Quit End If ' Determine if the selected site is an FTP site by checking the bindings. Set siteElement = sitesCollection(siteElementPos) Set bindingsCollection = siteElement.ChildElements.Item("bindings").Collection For bindingsCount = 0 To CInt(bindingsCollection.Count)-1 Set bindingElement = bindingsCollection(bindingsCount) If StrComp(CStr(bindingElement.Properties.Item("protocol").Value),"ftp",vbTextCompare)=0 Then isFtpSite = True Exit For End If Next ' If it's an FTP site, start and stop the site. If isFtpSite = True Then Set ftpServerElement = siteElement.ChildElements.Item("ftpServer") ' Create an instance of the Stop method. Set stopFtpSite = ftpServerElement.Methods.Item("Stop").CreateInstance() ' Execute the method to stop the FTP site. stopFtpSite.Execute() ' Create an instance of the Start method. Set startFtpSite = ftpServerElement.Methods.Item("Start").CreateInstance() ' Execute the method to start the FTP site. startFtpSite.Execute() End If ' Locate and return the index for a specific element in a collection. Function FindElement(collection, elementTagName, valuesToMatch) For i = 0 To CInt(collection.Count) - 1 Set elem = collection.Item(i) If elem.Name = elementTagName Then matches = True For iVal = 0 To UBound(valuesToMatch) Step 2 Set prop = elem.GetPropertyByName(valuesToMatch(iVal)) value = prop.Value If Not IsNull(value) Then value = CStr(value) End If If Not value = CStr(valuesToMatch(iVal + 1)) Then matches = False Exit For End If Next If matches Then Exit For End If End If Next If matches Then FindElement = i Else FindElement = -1 End If End Function
I hope this helps!
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
05 September 2012 • by Bob • IIS 8, IIS News Item, Windows Server 2012
Following up on today's public release of Microsoft Windows Server 2012 and Internet Information Services 8.0, you'll notice some big changes on the IIS.net website.
Over the past few months, we've been working hard with several partners to roll out a brand-new design for the IIS.net website that resembles more closely the look and feel of our websites for Microsoft Azure, Windows Server 2012, and Visual Studio 2012.
04 September 2012 • by Bob • IIS News Item, Windows Server 2012, IIS 8
Microsoft has just released Windows Server 2012! You can find out more about this release on the Official Windows Server 2012 Launch Website (http://www.windows-server-launch.com).
In tandem with the release of Windows Server 2012, the IIS team is happy to announce the general availability of Internet Information Services 8.0 This new version of IIS offers a wealth of new features and improvements, and here are just a few of the enhancements that you can expect in IIS 8.0: Application Initialization, Dynamic IP Address Restrictions, Centralized SSL Certificate Store, CPU Throttling, FTP Logon Attempt Restrictions, Server Name Indication (SNI) Support, Improved SSL and Configuration Scalability, support for Multicore Scaling on NUMA Hardware, and more! Additional information about IIS 8.0 is available in the "What's New in IIS 8.0 for Windows 8?" web page.
If you'd like to try IIS 8.0 for yourself, you can download the evaluation version and start experimenting today!
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/
01 September 2012 • by Bob • FTP, IIS, Windows
The folks in the TechEd group have uploaded the video from my "What's New with Internet Information Services (IIS) 8: Performance, Scalability, and Security Features" presentation to YouTube, so you can view the video online.
You can also download the slides and the WMV/MP4 for my presentation at the following URL:
http://channel9.msdn.com/Events/TechEd/NorthAmerica/2012/WSV332
One quick side note: around 38:55 during the video, I had just asked the audience if anyone had used the IIS Configuration Editor, when a tremendous thunderclap resounded outside - this prompted a great laugh from audience members. After the presentation had ended, a couple people came up and jokingly asked how I had managed to stage that so well.
26 July 2012 • by Bob • IIS, PHP, WinCache
The IIS team has officially signed off on the Windows Cache Extension (WinCache) version 1.3 for PHP 5.4, and the files have been uploaded to SourceForge. This version addresses all of the problems that were identified with WinCache 1.1 that customers were seeing after they upgraded their systems from PHP 5.3 to PHP 5.4.
With that in mind, you can download WinCache 1.3 for for PHP 5.4 from the following URL:
http://sourceforge.net/projects/wincache/files/wincache-1.3.4/
You can discuss WinCache 1.1 and WinCache 1.3 in the Windows Cache Extension for PHP forum on Microsoft's IIS.net website.
Since WinCache is an open source project, the IIS team has uploaded the pre-release source code for WinCache at the following URL:
http://pecl.php.net/package/WinCache
For the instructions on how to build the extension yourself, please refer to the Building WinCache Extension documentation.
Note: This blog was originally posted at http://blogs.msdn.com/robert_mcmurray/