jueves, 29 de mayo de 2014

Agregar o Editar Binding en IIS con AppCmd.exe o C#

Hola a todos, he estado jugando con IIS 8 (Windows 8) y me he puesto a ver como agregar y editar un Binding mediante línea de comandos o por código C#.

Sabemos que un Binding es un elemento que permite configurar información requerida por los request que se quieren comunicar con un sitio Web. Un sitio web en IIS puede tener N bindings. Es especialmente útil cuando quieres que diferentes direcciones de sitios web usen un sitio web (archicos y clases .net) unico. Por ejemplo, un holding de empresas que poseen diferentes URL pero el servidor que las procesa es único.
Un binding se conforma de: IP:Puerto:HostName

Vamos por partes, primero creamos un sitio Web vacio y dejamos el binding por defecto:

Ahora, agregamos otro binding para ese mismo sitio desde el mismo IIS para hacer la prueba de lo facil que es usando Add:

Ahora empieza lo interesante, vamos agregar un tercer binding por línea de comandos usando appcmd.exe.
Viendo el sitio oficial de Technet, no dice mucho, da un ejemplo básico de como usarlo en la línea de comandos.
Ejecuta el Command prompt en Modo Administrador sino con algunas instrucciones te puede dar el error:
Cannot read configuration file due to insufficient permissions 

Partamos, ejecutando la ayuda del comando SET de appcmd:
> C:\Windows\System32\inetsrv\appcmd.exe set site "test" /?
Ahí verás las opciones de SET, arriba se ven las opciones para Binding:
Luego ejecuta:
> C:\Windows\system32>C:\Windows\System32\inetsrv\appcmd.exe set site /site.name:test /+bindings.[protocol='http',bindingInformation='*:80:mitest']

Donde: test es mi sitio web ya configurado, http es mi protocolo y *:80:mitest es el binding en si: * es todas las IP, 80 es el puerto y mitest es el nombre del nuevo binding.
Con el + agregas binding a la colección, con el - eliminas.
No puedes ejecutar directamente > appcmd set ... como en el ejemplo ya que no está en las variables de ambiente pordefecto, así que debes usar la rura completa C:\Windows\inetsrv.

Ahora ahora todo ok, tenemos 3 binding.

Si tratamos de modificar un binding por línea de comandos, nos encontramos con el primer problema, no tiene ID (un BindingID), tienes que buscarlo, para luego modificarlo. Si ejecutaste la ayuda de SET, te darás cuenta que no hay algun comando que ayude (al menos no lo encontré).

Ahora veamos como agregar por linea de comandos pero ejecutado por código C#:
protected void Button1_Click(object sender, EventArgs e)
{
 string windir = Environment.GetEnvironmentVariable("windir");
 
 string comando = windir +"\\System32\\inetsrv\\appcmd.exe set site /site.name:test /+bindings.[protocol='http',bindingInformation='*:80:mitest']";
     
 ExecuteCommandAsync(comando);
}

public void ExecuteCommandSync(object command)
{
    try
    {
        System.Diagnostics.ProcessStartInfo procStartInfo = new System.Diagnostics.ProcessStartInfo("cmd", "/c " + command);
       
        procStartInfo.RedirectStandardOutput = true;
        procStartInfo.UseShellExecute = false;                
        procStartInfo.CreateNoWindow = true;
        procStartInfo.WindowStyle = ProcessWindowStyle.Hidden;

        System.Diagnostics.Process proc = new System.Diagnostics.Process();
        proc.StartInfo = procStartInfo;
        proc.Start();
        
        string result = proc.StandardOutput.ReadToEnd();
        
        Console.WriteLine(result);
        Debug.WriteLine(result);
    }
    catch (Exception objException)
    {
        Debug.WriteLine("Error:" + objException.Message);
    }
}

public void ExecuteCommandAsync(string command)
{
    try
    {
        
        Thread objThread = new Thread(new ParameterizedThreadStart(ExecuteCommandSync));
        
        objThread.IsBackground = true;
        
        objThread.Priority = ThreadPriority.Normal;
        
        objThread.Start(command);
    }
    catch (ThreadStartException objException)
    {
        Debug.WriteLine("Error:" + objException.Message);
    }
    catch (ThreadAbortException objException)
    {
        Debug.WriteLine("Error:" + objException.Message);
    }
    catch (Exception objException)
    {
        Debug.WriteLine("Error:" + objException.Message);
        
    }
}
El problema acá, es un tema muy comentado en foros de ayuda como StackOverflow, ya que da un maldito error de permisos:
Cannot read configuration file due to insufficient permissions.
Ya sea ejecutando el Visual Studio como administrador, o dando permisos en la carpeta inetsrv no pude sacar este error. Así que no lo dejé ahí y busque otra vía.

Ya que todo lo anterior no funcionó, queda la alternativa de encontrar alguna clase maestra. Vamos a listar, agregar y modificar via código C# con la clase ServerManager.
Primero, en tu proyecto en Visual Studio debes referenciar a la DLL: C:\Windows\System32\inetsrv\Microsoft.Web.Administration.dll.
Lo otro es que el Pool confugurado en el sitio Web en el IIS, en mi caso uso DefaultAppPool, debe estar correctamente configurado lo que es Identity, sino te dará errores de permiso.
Por ultimo usamos la clase ServerManager donde podemos manipular AppPools, Sitios o Binding.

Por ejemplo, con ServerManager tenemos todo el control y podemos recorrer los Sitios y los AppPools de IIS:
using (ServerManager serverManager = new ServerManager())
{
    if (serverManager.ApplicationPools == null)
        return;

    for (int i = 0; i < serverManager.Sites.Count; i++)
    {
        Microsoft.Web.Administration.ApplicationPoolCollection appPoolCollection = serverManager.ApplicationPools[i];
    }

    if (serverManager.Sites == null)
        return;

    for (int i = 0; i < serverManager.Sites.Count; i++)
    { 
       Microsoft.Web.Administration.BindingCollection bindingCollection = serverManager.Sites[i].Bindings;
    }
}
Si queremos listar todos los bindings e información de todos los sitios instalados en IIS, es simple:
public void ListBinding()
{
    Label1.Text = "";
    using (ServerManager serverManager = new ServerManager())
    {      
        if (serverManager.Sites == null)
            return;

        for (int i = 0; i < serverManager.Sites.Count; i++)
        {
            Debug.WriteLine(Environment.NewLine);
            Debug.WriteLine(serverManager.Sites[i].Id.ToString() + " - " + serverManager.Sites[i].Name);

            Label1.Text += "\r\n" + "[" + serverManager.Sites[i].Id.ToString() + " - " + serverManager.Sites[i].Name + "]";

            Microsoft.Web.Administration.BindingCollection bindingCollection = serverManager.Sites[i].Bindings;

            for (int j = 0; j < bindingCollection.Count; j++)
            {
                Debug.WriteLine(bindingCollection[j].Host +" " + bindingCollection[j].BindingInformation);

                Label1.Text += " Binding (" + j.ToString() + "): " + bindingCollection[j].Host + " " + bindingCollection[j].BindingInformation;                
            }
        }
    }
}
Este es el código que permite agregar un binding a un sitio dado. En mi caso uso un "keyName" que es el nombre del nuevo elemento (un nuevo sub-sitio que puede cambiar su información de binding). También requiere el ID del sitio Web:
public void AddBinding(int id, SiteBinding siteBinding, string KeyName)
{
    using (ServerManager serverManager = new ServerManager())
    {
        if (serverManager.Sites == null)
            return;

        for (int i = 0; i < serverManager.Sites.Count; i++)
        {
            if (serverManager.Sites[i].Id == id)
            {
                Microsoft.Web.Administration.BindingCollection bindingCollection = serverManager.Sites[i].Bindings;
                
                Microsoft.Web.Administration.Binding binding = serverManager.Sites[i].Bindings.CreateElement("binding");

                binding["protocol"] = siteBinding.Protocol;

                binding["bindingInformation"] = string.Format(@"{0}:{1}:{2}", siteBinding.IPAddress, siteBinding.Port.ToString(), siteBinding.HostName);

                bool existe = false;
                for (int j = 0; j < bindingCollection.Count; j++)
                {
                    if (bindingCollection[j].Host == siteBinding.HostName)
                    {
                        existe = true;
                        break;
                    }
                }

                if (existe == false)
                {
                    bindingCollection.Add(binding);
                    serverManager.CommitChanges();
                }

                if (bindingNameBase.ContainsKey(newKeyName) == false)
                {
                    bindingNameBase.Add(newKeyName, siteBinding.HostName);
                }

                
            }
        }
    }
}
El código para editar un binding existente de un sitio web existente. Uso el hashtable para mantener los IDs de los Binding:
public void EditBinding(int id, SiteBinding siteBinding, string keyName)
{
    using (ServerManager serverManager = new ServerManager())
    {
        if (serverManager.Sites == null)
            return;

        for (int i = 0; i < serverManager.Sites.Count; i++)
        {
            if (serverManager.Sites[i].Id == id)
            {
                Microsoft.Web.Administration.BindingCollection bindingCollection = serverManager.Sites[i].Bindings;

                // se elimina el binding
                Microsoft.Web.Administration.Binding bindingTmp = null;
                for (int j = 0; j < bindingCollection.Count; j++)
                {
                    if (bindingCollection[j].Host == bindingNameBase[keyName].ToString())
                    {
                        bindingTmp = bindingCollection[j];                                
                        break;
                    }
                }

                if (bindingTmp != null)
                {
                    bindingCollection.Remove(bindingTmp);

                    //se crea de nuevo
                    Microsoft.Web.Administration.Binding binding = serverManager.Sites[i].Bindings.CreateElement("binding");
                    binding["protocol"] = siteBinding.Protocol;
                    binding["bindingInformation"] = string.Format(@"{0}:{1}:{2}", siteBinding.IPAddress, siteBinding.Port.ToString(), siteBinding.HostName);

                    bool existe = false;
                    for (int j = 0; j < bindingCollection.Count; j++)
                    {
                        if (bindingCollection[j].Host == siteBinding.HostName)
                        {
                            existe = true;
                            break;
                        }
                    }

                    if (existe == false)
                    {
                        bindingCollection.Add(binding);
                        serverManager.CommitChanges();

                        bindingNameBase[keyName] = siteBinding.HostName;
                    }
                }                        
            }
        }
    }
}
Este código se puede refactorizar, (para los más quisquillosos), es solo una muestra de la capacidad de la clase ServerManager.

En resumen, tenemos:

Con IIS Manager:
  • Se puede agregar un binding.
  • Se puede modificar un binding.
  • Se puede eliminar un binding.

Con AppCmd.exe desde Command Promt:
  • Se puede agregar un binding.
  • No se puede modificar un binding de forma fácil (se debe poder pero no lo encontré, si alguien conoce la solución, que lo coloque en los comentarios).
  • Se puede eliminar un binding.

Con AppCmd.exe desde C#:
  • No se puede hacer nada, da error de permisos. Si alguien conoce la solución, que lo coloque en los comentarios.
Con ServerManager desde C#:
  • Se puede agregar un binding.
  • Se puede modificar un binding.
  • Se puede eliminar un binding.
Espero les haya servido. Nos vemos!

Links útiles: