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