Tritac (Master) Programmatically configure server tools (IIS, task scheduler, FileZilla server, firewall) with C#

Programmatically configure server tools (IIS, task scheduler, FileZilla server, firewall) with C#

15 februari 2017  |  Developer

For automatically deploying and updating a project on the server, you can programmatically configure various server tools with C# in .NET.

IIS

You can use 'Microsoft.Web.Administration' NuGet package to access your site configuration in IIS. For example, if you want to change to physical path from one folder (oldVersion) to another (newVersion):

        using Microsoft.Web.Administration;

       ..

        private static void ChangeSite(string oldVersion, string newVersion, ServerManager iis, string siteName) {
            var site = iis.Sites[siteName];
            if (site == null) {
                throw new ArgumentException("No site '" + siteName + "' found");
            }
            var path = site.Applications["/"].VirtualDirectories["/"].PhysicalPath;
            path = path.Replace(oldVersion, newVersion);
            site.Applications["/"].VirtualDirectories["/"].PhysicalPath = path;
            iis.CommitChanges();
        }

The ServerManager object provides you access to the Sites, indexed on the site name in IIS. You can then retrieve the physical path, change it and commit the changes.

Task scheduler

It is not uncommon to have a console application beside your web site/application to perform some scheduled tasks. You can use the 'Microsoft.Win32.TaskScheduler' NuGet package to access the task scheduler. Suppose you want to change the directory of an action of a task, from an old to a new version:

        using Microsoft.Win32.TaskScheduler;

        private static void ChangePath(string oldVersion, string newVersion, TaskService ts, string taskName) {
            var task = ts.GetTask(taskName);
            if (task == null) {
                throw new ArgumentException("Task '" + taskName + "' not found");
            }
            var action = (ExecAction) task.Definition.Actions[0];
            string path = action.Path;
            path = action.Path.Replace(oldVersion, newVersion);
            action.Path = path;
            ts.RootFolder.RegisterTaskDefinition(taskName, task.Definition, TaskCreation.Update, ADMIN_USERNAME, ADMIN_PASSWORD);
        }

The main object TaskService provides a function to get the task by name (which, as you know, cannot be changed, so it's can be easily used as an identifier). From the task definition, you can access the first action, retrieve its path, change it and commit the changes by reregistering. For updating a task, you have to provide a username and password of an administrator account (unfortunately).

FileZilla Server

Suppose you want your application to create some ftp accounts as well. The FileZilla Server configuration is stored in the FileZilla Server.xml in the FileZilla directory under Program Files. From this XML, you can generate a xsd schema and then a C# class with xsd.exe for simple processing. The root class is called 'FileZillaServer' in the example. For example, create a new ftp account based on an existing one:

        public string CreateNewUser(string filezillaServerFile, string filezillaServerLoc, string filezillaServerPath, string alreadyExistingName, string newName, string newPassword) {
            // make backup
            var xml = File.ReadAllText(filezillaServerFile);
            File.WriteAllText(filezillaServerFile + "." + DateTime.Now.ToString("yyyyMMdd_hhmmss") + ".bak", xml);
            // read and edit
            var xmlSerializer = new XmlSerializer(typeof(FileZillaServer));
            var root =(FileZillaServer) xmlSerializer.Deserialize(new XmlTextReader(new StringReader(xml)));

            FileZillaServerUsersUser newUser = ObjectHelper.Copy(root.Users.Where(u => u.Name.StartsWith(alreadyExistingName)).Last());
            newUser.Name = newName;

            newUser.Option.FirstOrDefault(o => o.Name == "Pass").Value = newPassword;     // password = md5 hash of the password
            string newDir = Path.Combine(filezillaServerLoc, newName);
            newUser.Permissions.FirstOrDefault().Dir = newDir;
            var userList = root.Users.ToList();
            userList.Add(newUser);
            root.Users = userList.ToArray();

            // create folder on server
            Directory.CreateDirectory(newDir);

            // write new file
            var newXml = NodeToXML(root);
            File.WriteAllText(filezillaServerFile, newXml);

            // load to FileZilla Server (you need to be admin to do this)
            System.Diagnostics.Process.Start("CMD.exe", "/C " + filezillaServerPath + "\\\"FileZilla Server.exe\" /reload-config");

            return newName;
        }

You can read the XML, make a backup just in case, change it and save it again. You have to tell the server to reload its config to make it work. The password field in the XML is a MD5 hash of the password. Maybe you can build a check to see if the new name does not already exists (this is left as an exercise). Note the application needs access to server file and administrator privileges to reload the configuration, so the easiest way is to use a scheduled console application.

Windows firewall

You probably need to whilelist some ips for the new ftp account. You can also do this programmatically by adding the ip to the FTP inbound rule in the server's firewall. For this, you need to reference 'C:\Windows\System32\hnetcfg.dll'. You can than create a COM object to access the firewall policies:

        using NetFwTypeLib;


        public void WhitelistFtpIp(string ftpRuleName, string ftpIps) {
            INetFwPolicy2 firewallPolicy =
                (INetFwPolicy2)Activator.CreateInstance(Type.GetTypeFromProgID("HNetCfg.FwPolicy2"));
            var rule = firewallPolicy.Rules.Item(ftpRuleName);
            var addrList = rule.RemoteAddresses.Split(',').ToList();
            string ip;
            var ipList = ftpIps.Split(',').Select(i => i.Trim());
            foreach (var ipFromList in ipList) {
                ip = ipFromList;
                if (!ip.Contains("/")) {
                    ip = ip + "/255.255.255.255";
                }
                if (!addrList.Contains(ip)) {
                    addrList.Add(ip);
                }
            }
            rule.RemoteAddresses = string.Join(",", addrList);

        }

From the policy object, you can find the rule by name, check to see if the IP is not already contained in the comma separated list and, if not, add it. I noticed when reading the ips that plain ips get /255.255.255.255 after them in the code (even though in the server interface, this is not visible/present), No need to commit changes, setting the rule is enough if the application has administrator privileges (so the easiest way is again to use a scheduled console application). If you have the desktop interface open on the server, you have to close and reopen it, to see the changes, though.