Sunday 17 June 2012

Installing Windows Azure FTP Cloud using both ACTIVE and PASSIVE mode

On a recent project there was a need to create a ftpsite using Windows Azure as the cloud service. By default Windows Azure does not support ftp. This blog describes how to install FTP using the WebRole.cs and specifically the public override bool OnStart(). It is done here so that if the Azure Virtual Machine is recycled the setup will be replicated in the new recycled virtual machine.

The first thing you will need is the FTP msi for IIS 7 / IIS 7.5. This is downloadable from http://go.microsoft.com/fwlink/?LinkID=143197
This can be put in blob azure storage or on the website itself. Lets just put it in the website under msi for now.

The following support methods are written which will be called by the public override bool OnStart().

1:ExecInProcess (This function is used to execute a command on the Azure Server)
2:GetExternalIP (Used for FTP in Passive Mode - Returns the External IP address) This function uses the public available http://checkip.dyndns.org/
public string ExecInProcess(string strCmd, string strParameters)
{
               try
            {
                               Trace.TraceInformation(string.Format("EXEC CMD {0} {1}", strCmd, strParameters));
                               ProcessStartInfo psipkg = new ProcessStartInfo(strCmd, strParameters);
                               psipkg.UseShellExecute = true;
                               psipkg.RedirectStandardOutput = false;
                               psipkg.RedirectStandardInput = false;
                               psipkg.RedirectStandardError = false;
                               Process procpkg = Process.Start(psipkg);
                               while (!procpkg.HasExited)
                                             System.Threading.Thread.Sleep(100);
               }
                catch (Exception ex)
                {
                                Trace.TraceError("ERROR: ExecInProcess. Cmd: {0} Params: {1} \r\n Details: {2}", strCmd, strParameters, ex.ToString());
               }
                return string.Empty;
}
public static string GetExternalIP()
{
               Regex regReplace = new Regex(@"[^\d\.]*", RegexOptions.Multiline|RegexOptions.IgnorePatternWhitespace);
                String direction = "";
               WebRequest request = WebRequest.Create(@http://checkip.dyndns.org/);
               using (WebResponse response = request.GetResponse())
               {
                              using (StreamReader stream = new StreamReader(response.GetResponseStream()))
                               {
                                               direction = stream.ReadToEnd();
                                }
                }
                direction = regReplace.Replace(direction, "");
                return direction;
}
Also we need to plan by setting up the ports to be opened etc. We are only allowed a maximum of 20 ports.Right client the Windows Azure Role

In the Settings TAB we will specify the FTP Values needed for configuration. See screen shot below.


In the Endpoint TAB we will need to specify both port 80/21/20000-20019. See screen shot below.

 
Then lets begin coding the WebRole.cs OnStart Function. But first let me describe what we hope to accomplish in the OnStart Function.

1: We retrieve the settings from the above using the RoleEnvironment class.
2: We determine where the website is installed. Usually drive E but when recycled drive F. I have put in drive C for good measure but that is unlikely. The code here could be better but I will leave it for now.
3: I Install the FTP7.5.msi for IIS 7. I exec the msiexec.exe to do this, obviously in silent mode.
4: I add the new ftp user using net.exe.
5: I grant the user access.
6: I create the FTP site
7: I allow SSL communication for good measure.
8: I configure the firewall to Allow FTP communication on port 21.
9: I then configure users to allow the specified user ftp access.
10: If I am to use passive mode I specify the range of ports to use. Note the limit in azure is 20. Also I set the external facing ip address to the ftp server configuration needed to run in passive mode.
11: I once again grant access to users
12: I configure the firewall to give ftp service full access.
13: I setup the ftp ssl authorization.
14: I restart the ftp service to reflect the changes made.

Now for the code

public override bool OnStart()
{
string strFtpUser = RoleEnvironment.GetConfigurationSettingValue("FtpUser");
string strFtpUserPassword = RoleEnvironment.GetConfigurationSettingValue("FtpUserPassword");
string strLowPort = RoleEnvironment.GetConfigurationSettingValue("FtpPassiveLowPortRange");
string strHighPort = RoleEnvironment.GetConfigurationSettingValue("FtpPassiveHighPortRange");
string strFTPMode = RoleEnvironment.GetConfigurationSettingValue("FtpMode");//ACTIVE or PASSIVE
string strIP = GetIP();//THIS IS INACCURATE:::RoleEnvironment.CurrentRoleInstance.InstanceEndpoints.ElementAt(0).Value.IPEndpoint.Address.ToString();
string path = @"E:\FTP";
string msiPath = @"E:\sitesroot\0\MSI\FTPIIS.msi";
try
{
       Directory.CreateDirectory(path);
       Directory.CreateDirectory(msiPath);
}
catch (Exception)
{
       path = @"F:\FTP";
       string msiPath = @"F:\sitesroot\0\MSI\FTPIIS.msi";
       try
       {
             Directory.CreateDirectory(path);
             Directory.CreateDirectory(msiPath);
       }
       catch (Exception)
       {
                path = @"C:\FTP";
                string msiPath = @"C:\sitesroot\0\MSI\FTPIIS.msi";
                try
                {
                       Directory.CreateDirectory(path);
                       Directory.CreateDirectory(msiPath);
                 }
                 catch (Exception){ }
        }
}
ExecInProcess(Environment.GetEnvironmentVariable("WINDIR") + @"\system32\msiexec.exe", @" /i " + msiPath + @" /passive /l* " + msiPath + @".log");
//ADD The User to the access control list
ExecInProcess(@"net.exe", @"user " + strFtpUser + " " + strFtpUserPassword + @" /Add");
ExecInProcess(@"icacls.exe", path + @" /grant " + strFtpUser + @":(OI)(CI)F");
//ADD The FTP Site
ExecInProcess(Environment.GetEnvironmentVariable("WINDIR") + @"\system32\inetsrv\appcmd.exe", @"add site /site.name:MicroangeloFTPServer /+bindings.[protocol='ftp',bindinginformation='*:21:'] /physicalpath:" + path );
//ADD The FTPS Service
ExecInProcess(Environment.GetEnvironmentVariable("WINDIR") + @"\system32\inetsrv\appcmd.exe" , @"set config -section:system.applicationHost/sites /siteDefaults.ftpServer.security.ssl.controlChannelPolicy:SslAllow /commit:apphost");
ExecInProcess(Environment.GetEnvironmentVariable("WINDIR") + @"\system32\inetsrv\appcmd.exe" , @"set config -section:system.applicationHost/sites /siteDefaults.ftpServer.security.ssl.dataChannelPolicy:SslAllow /commit:apphost");
ExecInProcess(Environment.GetEnvironmentVariable("WINDIR") + @"\system32\inetsrv\appcmd.exe" , @"set config -section:system.applicationHost/sites /siteDefaults.ftpServer.security.authentication.basicAuthentication.enabled:true");
//Configure Firewall
ExecInProcess(@"netsh.exe", @"advfirewall firewall add rule name=""AllowFTP"" protocol=TCP dir=in localport=21 action=allow" );
//Configure Users
ExecInProcess(Environment.GetEnvironmentVariable("WINDIR") + @"\system32\inetsrv\appcmd.exe" , @"set config MicroangeloFTPServer /section:system.ftpserver/security/authorization /+[accessType='Allow',permissions='Read,Write',roles='',users='" + strFtpUser + @"'] /commit:apphost");
if (strFTPMode.ToUpper().Contains("PASSIVE"))
{
ExecInProcess(Environment.GetEnvironmentVariable("WINDIR") + @"\system32\inetsrv\appcmd.exe", @"set config /section:system.ftpServer/firewallSupport /lowDataChannelPort:" + strLowPort + @" /highDataChannelPort:" + strHighPort);
ExecInProcess(Environment.GetEnvironmentVariable("WINDIR") + @"\system32\inetsrv\appcmd.exe", @"set config /section:system.applicationHost/sites /siteDefaults.ftpServer.firewallSupport.externalIp4Address:" + strIP);
}
//Configure Users
ExecInProcess(Environment.GetEnvironmentVariable("WINDIR") + @"\system32\inetsrv\appcmd.exe" , @"set config MicroangeloFTPServer /section:system.ftpserver/security/authorization /+[accessType='Allow',permissions='Read,Write',roles='',users='*'] /commit:apphost");
//Configure Firewall
ExecInProcess(@"netsh.exe", @"advfirewall set global StatefulFtp enable");
ExecInProcess(Environment.GetEnvironmentVariable("WINDIR") + @"\system32\inetsrv\appcmd.exe", @"set config -section:system.applicationHost/sites /siteDefaults.ftpServer.security.ssl.controlChannelPolicy:SslAllow /commit:apphost");
ExecInProcess(Environment.GetEnvironmentVariable("WINDIR") + @"\system32\inetsrv\appcmd.exe", @"set config -section:system.applicationHost/sites /siteDefaults.ftpServer.security.ssl.dataChannelPolicy:SslAllow /commit:apphost");
ExecInProcess(@"net.exe", @"stop ftpsvc");
ExecInProcess(@"net.exe", @"start ftpsvc");
return base.OnStart();
}


Now you have Azure FTP Cloud Solution both Active/Passive using Windows Azure as the hosting platform.

Enjoy

2 comments:

  1. Hi,

    Over the past few days I've been trying to create FTP server in a web role without success....
    Where in your code do you install the FTP server and service features ?
    Does your solution works on server 2012 also ?
    Can you provide a solution that works ? :) I can't make it work and the my boss starts to be cranky :)

    ReplyDelete
  2. Hello Israel,

    I have provided very detail instructions in my explanation above.

    For a small fee I can setup a web role as a ftp server for you on your azure platform if you wish.

    You can email me at angelo.laris*gmail.com if you wish to proceed.

    Regards

    Angelo Laris

    ReplyDelete