HTTP/1.1 500 Internal Server Error Date: Fri, 30 Nov 2012 12:15:15 GMT Server: Apache/2.2.22 (Win32) Content-Length: 547 Connection: close Content-Type: text/html; charset=iso-8859-1 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>500 Internal Server Error</title> </head><body> < h1>Internal Server Error</h1> <p>The server encountered an internal error or misconfiguration and was unable to complete your request.</p> </body></HTML>
using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
namespace DigestClient
{
publicclassDigestHttpWebRequest
privatestring _user;
privatestring _password;
privatestring _realm;
privatestring _nonce;
privatestring _qop;
privatestring _cnonce;
privateAlgorithm _md5;
privateDateTime _cnonceDate;
privateint _nc;
privatestring _requestMethod = WebRequestMethods.Http.Get;
privatestring _contentType;
privatebyte[] _postData;
public DigestHttpWebRequest(string user, string password)
_user = user;
_password = password;
}
publicstring Method
get { return _requestMethod; }
set { _requestMethod = value; }
publicstring ContentType
get { return _contentType; }
set { _contentType = value; }
publicbyte[] PostData
get { return _postData; }
set { _postData = value; }
publicHttpWebResponse GetResponse(Uri uri)
HttpWebResponse response = null;
int infiniteLoopCounter = 0;
int maxNumberAttempts = 2;
while ((response == null ||
response.StatusCode != HttpStatusCode.Accepted) &&
infiniteLoopCounter < maxNumberAttempts)
try
var request = CreateHttpWebRequestObject(uri);
// If we've got a recent Auth header, re-use it!
if (!string.IsNullOrEmpty(_cnonce) &&
DateTime.Now.Subtract(_cnonceDate).TotalHours< 1.0)
request.Headers.Add("Authorization", ComputeDigestHeader(uri));
response = (HttpWebResponse)request.GetResponse();
catch (WebException webException)
// Try to fix a 401 exception by adding a Authorization header
if (webException.Response != null &&
((HttpWebResponse)webException.Response).StatusCode == HttpStatusCode.Unauthorized)
var wwwAuthenticateHeader = webException.Response.Headers["WWW-Authenticate"];
_realm = GetDigestHeaderAttribute("realm", wwwAuthenticateHeader);
_nonce = GetDigestHeaderAttribute("nonce", wwwAuthenticateHeader);
_qop = GetDigestHeaderAttribute("qop", wwwAuthenticateHeader);
_md5 = GetMD5Algorithm(wwwAuthenticateHeader);
_nc = 0;
_cnonce = newRandom().Next(123400, 9999999).ToString();
_cnonceDate = DateTime.Now;
request = CreateHttpWebRequestObject(uri, true);
infiniteLoopCounter++;
else
throw webException;
switch (response.StatusCode)
caseHttpStatusCode.OK:
caseHttpStatusCode.Accepted:
return response;
caseHttpStatusCode.Redirect:
caseHttpStatusCode.Moved:
uri = newUri(response.Headers["Location"]);
// We decrement the loop counter, as there might be a variable number of redirections which we should follow
infiniteLoopCounter--;
break;
catch (WebException ex)
throw ex;
thrownewException("Error: Either authentication failed, authorization failed or the resource doesn't exist");
privateHttpWebRequest CreateHttpWebRequestObject(Uri uri, bool addAuthenticationHeader)
var request = (HttpWebRequest)WebRequest.Create(uri);
request.AllowAutoRedirect = false;
request.Method = this.Method;
if (!String.IsNullOrEmpty(this.ContentType))
request.ContentType = this.ContentType;
if (addAuthenticationHeader)
if (this.PostData != null && this.PostData.Length > 0)
request.ContentLength = this.PostData.Length;
Stream postDataStream = request.GetRequestStream(); //open connection
postDataStream.Write(this.PostData, 0, this.PostData.Length); // Send the data.
postDataStream.Close();
elseif (
this.Method == WebRequestMethods.Http.Post &&
(this.PostData == null || this.PostData.Length == 0))
request.ContentLength = 0;
return request;
privateHttpWebRequest CreateHttpWebRequestObject(Uri uri)
return CreateHttpWebRequestObject(uri, false);
privatestring ComputeDigestHeader(Uri uri)
_nc = _nc + 1;
string ha1, ha2;
switch (_md5)
caseAlgorithm.MD5sess:
var secret = ComputeMd5Hash(string.Format(CultureInfo.InvariantCulture, "{0}:{1}:{2}", _user, _realm, _password));
ha1 = ComputeMd5Hash(string.Format(CultureInfo.InvariantCulture, "{0}:{1}:{2}", secret, _nonce, _cnonce));
ha2 = ComputeMd5Hash(string.Format(CultureInfo.InvariantCulture, "{0}:{1}", this.Method, uri.PathAndQuery));
var data = string.Format(CultureInfo.InvariantCulture, "{0}:{1:00000000}:{2}:{3}:{4}",
_nonce,
_nc,
_cnonce,
_qop,
ha2);
var kd = ComputeMd5Hash(string.Format(CultureInfo.InvariantCulture, "{0}:{1}", ha1, data));
returnstring.Format("Digest username=\"{0}\", realm=\"{1}\", nonce=\"{2}\", uri=\"{3}\", " +
"algorithm=MD5-sess, response=\"{4}\", qop={5}, nc={6:00000000}, cnonce=\"{7}\"",
_user, _realm, _nonce, uri.PathAndQuery, kd, _qop, _nc, _cnonce);
caseAlgorithm.MD5:
ha1 = ComputeMd5Hash(string.Format("{0}:{1}:{2}", _user, _realm, _password));
ha2 = ComputeMd5Hash(string.Format("{0}:{1}", this.Method, uri.PathAndQuery));
var digestResponse =
ComputeMd5Hash(string.Format("{0}:{1}:{2:00000000}:{3}:{4}:{5}", ha1, _nonce, _nc, _cnonce, _qop, ha2));
"algorithm=MD5, response=\"{4}\", qop={5}, nc={6:00000000}, cnonce=\"{7}\"",
_user, _realm, _nonce, uri.PathAndQuery, digestResponse, _qop, _nc, _cnonce);
thrownewException("The digest header could not be generated");
privatestring GetDigestHeaderAttribute(string attributeName, string digestAuthHeader)
var regHeader = newRegex(string.Format(@"{0}=""([^""]*)""", attributeName));
var matchHeader = regHeader.Match(digestAuthHeader);
if (matchHeader.Success)
return matchHeader.Groups[1].Value;
thrownewApplicationException(string.Format("Header {0} not found", attributeName));
privateAlgorithm GetMD5Algorithm(string digestAuthHeader)
var md5Regex = newRegex(@"algorithm=(?<algo>.*)[,]", RegexOptions.IgnoreCase);
var md5Attribute = md5Regex.Match(digestAuthHeader);
if (md5Attribute.Success)
char[] charSeparator = newchar[] { ',' };
string algorithm = md5Attribute.Result("${algo}").ToLower().Split(charSeparator)[0];
switch (algorithm)
case"md5-sess":
case"\"md5-sess\"":
returnAlgorithm.MD5sess;
case"md5":
case"\"md5\"":
default:
returnAlgorithm.MD5;
thrownewApplicationException("Could not determine Digest algorithm to be used from the server response.");
privatestring ComputeMd5Hash(string input)
var inputBytes = Encoding.ASCII.GetBytes(input);
var hash = MD5.Create().ComputeHash(inputBytes);
var sb = newStringBuilder();
foreach (var b in hash)
sb.Append(b.ToString("x2"));
return sb.ToString();
publicenumAlgorithm
MD5 = 0, // Apache Default
MD5sess = 1 //IIS Default
Sample usage for HTTP GET requests:
DigestHttpWebRequest request = newDigestHttpWebRequest(username, password);
HttpWebResponse result = request.GetResponse(uri);
Sample usage for HTTP POST requests:
request.Method = WebRequestMethods.Http.Post;
request.ContentType = "text/plain; charset=utf-8";
request.PostData = Encoding.ASCII.GetBytes(postData);
Applies to Microsoft .NET Framework 1.1 Microsoft .NET Framework 2.0 Microsoft .NET Framework 3.0 Microsoft .NET Framework 3.5 Microsoft .NET Framework 4.0 Microsoft .NET Framework 4.5
Recientemente he trabajado en un caso en el que tras reiniciar un servidor Windows Server 2003, el servicio IIS Admin Service fallaba en el arranque con el siguiente mensaje de error:
Windows could not start the IIS Admin Service on Local Computer. For more information, review the System Event Log. If this is a non-Microsoft service, contact the service vendor, and refer to service-specific error code -2146893813.
Adicionalmente, en el log de eventos de sistema se registraba el siguiente error:
Manuel: Event Type: Error Event Source: Service Control Manager Event Category: None Event ID: 7024 Date: 23/05/2012 Time: 11:40:35 User: N/A Computer: IIS6-Server Description: The IIS Admin Service service terminated with service-specific error 2148073483 (0x8009000B).
El código de error 0x8009000B se corresponde a NTE_BAD_KEY_STATE (“Key not valid for use in specified state”o “Clave no válida para utilizar en el estado especificado” en castellano) e indica un error al utilizar unas claves de encriptación. IIS 6.0 cifra algunos campos del fichero XML de configuración (metabase.xml) con información sensible como contraseñas de cuentas de servicio. Para esto, IIS 6.0 crea un contenedor de claves criptográficas durante la instalación del servicio llamado “Microsoft Internet Information Server”. Este contenedor de claves está almacenado físicamente en el fichero %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\MachineKeys\c2319c42033a5ca7f44e731bfd3fa2b5_<MachineGuid>. El valor de MachineGuid se puede obtener de la siguiente clave del registro y será distinto en cada servidor:
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography]
"MachineGuid"="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Con la herramienta certutil podemos volcar cierta información de este contenedor de claves, y entre otras cosas podemos ver que las claves privadas no son exportables, por lo que el fichero metabase.xml cifrado con estas claves únicamente podrá ser descifrado en el mismo servidor donde fue creado.
C:\WINDOWS\system32>certutil -v -key "Microsoft Internet Information Server"
Microsoft Strong Cryptographic Provider:
Microsoft Internet Information Server
c2319c42033a5ca7f44e731bfd3fa2b5_809cf898-e06e-4673-83e2-6200ef0291cc
AT_SIGNATURE:
Key Id Hash(sha1): d0 31 ed 58 81 bf 61 b9 a3 76 99 cb 17 ad ce 88 ff b7 b8 cd
Container Public Key:
000030 48 02 41 00 cd c2 bab5 47 a7 2b 2f 57 4b ae
0010a7 fe 26 9c a2 ed f3 f958 36 df 8f 35 31 1c c0
002032 2c 35 8c 42 68 84 0dc2 4d dd 9a fd 5d 87 0f
0030b2 b9 33 d4 1b bc 62 647c bd d7 6c 59 6e c1 85
0040b3 46 73 2c 59 02 03 0100 01
Private key is NOT exportable
AT_KEYEXCHANGE:
Key Id Hash(sha1): 4f 50 e1 14 9f 63 73 0b ef ae 7c 34 e5 1d 6b d2 eb 1b 59 27
000030 48 02 41 00 a3 aa e0ee 9f 45 e9 b4 6c 4d 11
001044 96 39 f8 f6 51 ec 9014 60 1f 57 6e 10 d2 f4
002069 4c c1 1f 09 f5 69 e0fd 75 67 40 00 0d a0 4a
00307d c3 b5 a1 40 b9 20 a65a 3e 1e e9 28 bd aa 2c
00406e 62 64 c9 61 02 03 0100 01
CertUtil: -key command completed successfully.
IIS 6.0 permite generar backups de la metabase exportables/importables a cualquier servidor, para lo que se utiliza una contraseña en lugar de las claves RSA de máquina para cifrar la metabase. Tener un backup de la configuración de IIS protegido con contraseña puede resultar muy útil como veremos más adelante.
Volviendo al problema original, el error 0x8009000B al arrancar el servicio IIS Admin Service típicamente se debe a una corrupción del contenedor de claves de IIS, por lo que el servicio no es capaz de descifrar los campos relevantes al leer el fichero de configuración y por consecuencia fallan en el arranque. Llegados a esta situación, básicamente tenemos tres opciones, que por orden de preferencia son:
1) Restaurar un backup de system state en el que se restauraría una copia intacta del fichero contenedor de claves c2319c42033a5ca7f44e731bfd3fa2b5_<MachineGuid> lo que permitiría volver a arrancar el servicio de IIS Admin Service. 2) Reinstalar IIS y reconfigurarlo desde un backup de la metabase protegido por contraseña que podamos importar después de la reinstalación.
3) Reinstalar IIS y reconfigurarlo manualmente desde cero.
PROCESO DE REINSTALACIÓN Y RESTAURACIÓN DEL CONTENEDOR DE CLAVES En caso que fuera necesaria la reinstalación de IIS, los pasos a seguir son los siguientes:
2) Eliminar el fichero contenedor de claves de “Microsoft Internet Information Server” en la ruta“%ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\MachineKeys\c2319c42033a5ca7f44e731bfd3fa2b5_<MachineGuid>”.
3) Reinstalar IIS.
4) Importar el backup de la metabase protegido por contraseña.
5) Añadir las cuentas configuradas como identidad de los application pool al grupo de seguridad IIS_WPG (IIS Worker Process Group) de nuevo, dado que el grupo de seguridad se vuelve a crear tras la reinstalación.
Si tenemos varios servidores configurados idénticamente, técnicamente es posible importar un backup de la metabase que ha sido exportado desde otro servidor distinto. No obstante, esto implica tener que modificar manualmente algunas cosas adicionales como sincronizar las cuentas IUSR_<serverName> e IWAM_<serverName> y sus contraseñas, o modificar los bindings de los sitios web en caso de que escuchen por IPs distintas en los distintos servidores de la granja. En definitiva, haced backup del system state y de la metabase regularmente, y espero que nunca tengáis que hacer uso de este post.
- Daniel Mossberg
Recientemente he publicado una serie de artículos en MSDN España abordando una pregunta que recibimos con frecuencia. ¿Cuáles son las diferencias entre los tres modelos de programación de ASP.NET y en que situaciones debo elegir uno de ellos sobre los otros?
Estos son los enlaces a la serie completa:
ASP.NET Web Forms, MVC o Web Pages – Introducción
http://msdn.microsoft.com/es-es/asp.net/hh984851
Parte 1 – ASP.NET Web Forms
http://msdn.microsoft.com/es-es/asp.net/hh984854
Parte 2 – ASP.NET MVC
http://msdn.microsoft.com/es-es/asp.net/hh984855
Parte 3 – ASP.NET Web Pages
http://msdn.microsoft.com/es-es/asp.net/hh984856
Espero que os resulte interesante.
Pasados más de tres meses desde mi último post, ya es momento de retomarlo. Hoy voy a escribir sobre un problema con el que me he encontrado en varias ocasiones en los últimos meses. Básicamente, el problema se produce cuando tenemos una aplicación ASP.NET configurada para impersonar al usuario autenticado, y en algún punto de la aplicación se trata de impersonar programáticamente a un usuario distinto. En estas circunstancias, la aplicación va a fallar con el siguiente error:
Exception Details:
System.Web.HttpException: An error occurred while attempting to impersonate. Execution of this request cannot continue.
Stack Trace:
[HttpException (0x80004005): An error occurred while attempting to impersonate. Execution of this request cannot continue.]
System.Web.ImpersonationContext.GetCurrentToken() +8845961
System.Web.ImpersonationContext.get_CurrentThreadTokenExists() +58
System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +193
System.Web.ApplicationStepManager.ResumeSteps(Exception error) +501
System.Web.HttpApplication.System.Web.IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, Object extraData) +123
System.Web.HttpRuntime.ProcessRequestInternal(HttpWorkerRequest wr) +379
…
Básicamente, el problema radica en que al llamar al método Impersonate (o llamando directamente a la API LogonUser que es la que se termina llamando desde Impersonate), se intenta acceder al token del proceso (por ejemplo, w3wp.exe), y la cuenta del usuario autenticado (el que está siendo impersonado debido a la configuración del web.config) no tiene permiso para acceder a dicho token.
Por lo tanto, para poder impersonar una cuenta distinta programáticamente, necesitamos deshacer la impersonación inicial antes de impersonar una cuenta distinto. Para ello, podemos llamar a la API RevertToSelf para deshacer la impersonación antes de llamar a LogonUser.
En el siguiente ejemplo se ilustra cómo podríamos hacerlo:
const int LOGON32_PROVIDER_DEFAULT = 0;
const int LOGON32_LOGON_INTERACTIVE = 2;
const int SecurityImpersonation = 2;
int win32ErrorNumber = 0;
//Mostramos por pantalla la identidad inicial (la impersonada mediante en el web.config):
Response.Write("Identidad inicial: " + WindowsIdentity.GetCurrent().Name.ToString() + "<BR>");
//Guardamos esa identidad inicial para poder restaurarlo posteriormente
WindowsIdentity _initialIdentity = WindowsIdentity.GetCurrent();
//Llamamos a la API "RevertToSelf" para deshacer la impersonación configurada en el web.config:
RevertToSelf();
//Volvemos a mostrar la identidad, y ahora vemos que ha cambiado a la identidad del application
//pool: (NETWORK SERVICE) por defecto.
Response.Write("Después de llamar a RevertToSelf: " + WindowsIdentity.GetCurrent().Name.ToString() + "<BR>");
//Realizamos la impersonación deseada.
IntPtr _tokenHandle = IntPtr.Zero;
IntPtr _dupeTokenHandle = IntPtr.Zero;
string _domainname = "CONTOSO";
string _username = "demo";
string _password = "P@ssw0rd!";
if (!LogonUser(_username, _domainname, _password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, out _tokenHandle))
win32ErrorNumber = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
throw new Exception(win32ErrorNumber.ToString());
if (!DuplicateToken(_tokenHandle, SecurityImpersonation, out _dupeTokenHandle))
CloseHandle(_tokenHandle);
System.Security.Principal.WindowsIdentity newId = new System.Security.Principal.WindowsIdentity(_dupeTokenHandle);
WindowsImpersonationContext _impersonatedUser = newId.Impersonate();
//Mostramos por pantalla la identidad impersonada
Response.Write("Después de impersonar: " + WindowsIdentity.GetCurrent().Name.ToString() + "<BR>");
//Desahacemos la impersonación cuando ya no lo necesitemos.
_impersonatedUser.Undo();
//Mostramos por pantalla la identidad, que otra vez será la del application pool
Response.Write("Deshacer impersonación: " + WindowsIdentity.GetCurrent().Name.ToString() + "<BR>");
//Volvemos a impersonar con la identidad inicial (la del web.config) que hemos mantenido referenciada
//con la variable _impersonatedUser:
_impersonatedUser = _initialIdentity.Impersonate();
//Por último volvemos a mostrar la identidadMostramos por pantalla la identidad
Response.Write("Volver a impersonación inicial: " + WindowsIdentity.GetCurrent().Name.ToString() + "<BR>");
Tras ejecutar el código anterior, el resultado emitido por pantalla sería el siguiente (en mi ejemplo):
Identidad inicial: CONTOSO\danielDespués de llamar a RevertToSelf: NT AUTHORITY\NETWORK SERVICEDespués de impersonar: CONTOSO\demoDeshacer impersonación: NT AUTHORITY\NETWORK SERVICEVolver a impersonación inicial: CONTOSO\daniel
Estas son las signatures de C# para llamar a las APIs del ejemplo mediante platform invoke:
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LogonUser(
string lpszUsername,
string lpszDomain,
string lpszPassword,
int dwLogonType,
int dwLogonProvider,
out IntPtr phToken);
static extern bool RevertToSelf();
public extern static bool DuplicateToken(
IntPtr ExistingTokenHandle,
int SECURITY_IMPERSONATION_LEVEL,
out IntPtr DuplicateTokenHandle);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool CloseHandle(IntPtr hObject);
Espero que os sea de utilidad.
Cuando subimos ficheros a una aplicación ASP.NET, dependiendo del tamaño del fichero y la velocidad de la conexión del cliente, ocasionalmente la subida del fichero puede fallar con el mensaje de error en el cliente de Internet Explorer no puede mostrar la página web, o con alguna de las siguientes excepciones (dependiendo de cómo esté configurado IIS, habitualmente este detalle de la excepción únicamente lo podremos ver en el log de eventos NT del servidor):
HttpException: Request timed out.
HttpException: Maximum request length exceeded.
En estos casos, los parámetros que debéis configurar adecuadamente considerando el tamaño el tiempo de transmisión del fichero son los siguientes:
<httpRuntime executionTimeout="600" maxRequestLength="61440" />
El parámetro de executionTimeout determina el tiempo en segundos que puede ejecutarse una páginas ASP.NET antes de que se lanza una excepción de time out. Por otro lado, el parámetro maxRequestLength determina el tamaño total máximo en bytes de las peticiones HTTP. Debemos configurar estas secciones acorde con los tiempos máximos de carga y tamaños máximos de fichero que queremos que soporte nuestra aplicación.
Los parámetros de la sección <httpRuntime> se pueden configurar a nivel de web.config de aplicación, e incluso a nivel de una página específica de la siguiente manera:
<?xml version="1.0"?>
<configuration>
...
<location path="Default.aspx">
<!--
Aquí la configuración específica
para la página Default.aspx del
directorio virtual al que aplica
este web.config.
-->
<system.web>
<httpRuntime
executionTimeout="600"
maxRequestLength="61440" />
</system.web>
</location>
</configuration>
Esto último es recomendable, dado que aumentar el executionTimeout de todas las páginas de una aplicación puede tener consecuencias negativas. En general, los timeout se deben mantener con valores razonablemente bajos, precisamente para que cuando se produzca una situación de bloqueo, se lanze la excepción y se finalice el procesamiento de la página en poco tiempo de forma que se libere el thread y se puedan atender otras peticiones. Con timeouts elevados se podrían llegar a agotar todos los threads en situaciones de bloqueo, por tardar demasiado en ser liberados.
Por último, si una vez habiendo configurado nuestra aplicación con valores adecaudos, la carga de ficheros sigue fallando con errores HTTP 400 en los logs de IIS y errores de Timer_EntityBody en los logs de HTTP.sys (HTTPERR), probad a desinstalar la funcionalidad de SNP (Scalable Networking Pack) en el servidor web siguiente los instrucciones del siguiente artículo:
An update to turn off default SNP features is available for Windows Server 2003-based and Small Business Server 2003-based computers
http://support.microsoft.com/default.aspx?scid=kb;EN-US;948496
Estos cambios requieren reincio del sistema operativo para entrar en vigor.
Espero que os haya sido de utilidad.
Existe cierta confusión con la compatibilidad entre binarios de Silverlight y binarios de .NET, y a más de uno le pilla por sorpresa que en un proyecto de Visual Studio de una aplicación Silverlight, no se pueden referenciar bibliotecas de clases (class library) desarrolladas en .NET de escritorio (para diferenciarlo del .NET de Silverlight). El motivo es que el runtime y la biblioteca de clases de .NET para Silverlight es un subconjunto del runtime y la biblioteca de clases de .NET de escritorio, y por lo tanto no son directamente compatibles.
Se puede migrar el código de las clases .NET a clases .NET para Silverlight, y en ocasiones puede ser tan sencillo como recompilar el mismo código fuente (por ejemplo, como una Silverlight Class Library). En otras ocasiones esta migración no es directa, dado que la biblioteca de clases de Silverlight no contiene todas las clases que la biblioteca de clases del Framework .NET.
.NET Framework Class Library
http://msdn.microsoft.com/en-us/library/ms229335.aspx
.NET Framework Class Library for Silverlight
http://msdn.microsoft.com/en-us/library/cc838194.aspx
Supongamos que queremos reutilizar unas bibliotecas de clases que manejan objetos DataSet, y dado que .NET para Silverlight (hasta la versión Silverlight 4 RC) no contiene el namespace System.Data, no podemos simplemente recompilar el código fuente (fallaría la compilación dado que no estarían implementada ninguna clase DataSet).
Una posible implementación que nos permite reutilizar al máximo nuestras bibliotecas de clases .NET desde aplicaciones Silverlight sería implementar una interfaz, expuesta como un web service, y consumir dicho web service desde la aplicación Silverlight.
De esta manera, todo el código de .NET de escritorio se ejecutará en el servidor web, y la aplicación Silverlight que se ejecutará en el navegador del cliente únicamente representará la capa de presentación.
Os muestro un pequeño ejemplo en C# como prueba de concepto. En este ejemplo, el web service está implementado como un web service de ASP.NET 2.0 (*.asmx), pero considerad la posibilidad de implementarlo como un web service WCF (Windows Communication Foundation), que es la siguiente generación de web services de .NET.
Supongamos que tenemos la siguiente clase en nuestra biblioteca de lógica de negocio. Esta es la clase cuyo código nos gustaría reutilizar en Silverlight, pero dado que hace uso de System.Data no podemos recompilar el código sin más.
using System.Data;
namespace SilverlightTest.Web
public class BusinessLogic
public DataSet getCustomerData(int customerID)
DataSet dataSet = new DataSet();
DataTable dataTable = new DataTable();
dataTable.Columns.Add("Columna 1");
dataTable.Columns.Add("Columna 2");
dataTable.Rows.Add(new object[] { "celda 0-0", "celda 0-1" });
dataTable.Rows.Add(new object[] { "celda 1-0", "celda 1-1" });
dataTable.Rows.Add(new object[] { "celda 2-0", "celda 2-1" });
dataTable.Rows.Add(new object[] { "celda 3-0", "celda 3-1" });
dataSet.Tables.Add(dataTable);
return dataSet;
Para solventar este problema, podemos crear un web service que exponga el método getCustomerData. En este caso, para hacerlo compatible con Silverlight, convertiremos el objeto DataSet en uno o varios arrays bidimensionales (uno por cada DataTable del DataSet) antes de enviar la respuesta al cliente Silverlight:
using System.Web;
using System.Web.Services;
using SilverlightTest.Web;
[WebService]
public class SilverlightInterface : System.Web.Services.WebService
[WebMethod]
public object[][] getCustomerData(int customerID)
//Utilizamos la lógica de la biblioteca de clases de .NET de escritorio
BusinessLogic businessLogic = new BusinessLogic();
DataSet dataSet = businessLogic.getCustomerData(customerID);
int x = dataSet.Tables[0].Rows.Count;
int y = dataSet.Tables[0].Columns.Count;
//Declaramos y demensionamos el Array
object[][] dataArray = new object[x][];
for (int i = 0; i < x; i++)
dataArray[i] = new object[y];
//Rellenamos el Array con los datos del DataSet
for (int j = 0; j < y; j++)
dataArray[i][j] = dataSet.Tables[0].Rows[i][j];
return dataArray;
Por último, desde el cliente Silverlight consumimos el web service de la siguiente manera tras haber incluido la referencia web (que hemos llamado BusinessLogicWS):
public partial class SilverlightPage : UserControl
public SilverlightPage()
InitializeComponent();
//Creamos el objeto proxy para interactuar con el web service
BusinessLogicWS.SilverlightInterfaceSoapClient wsProxy =
new BusinessLogicWS.SilverlightInterfaceSoapClient();
wsProxy.getCustomerDataCompleted
+= new EventHandler
<BusinessLogicWS.getCustomerDataCompletedEventArgs>
(getCustomerDataCompletedHandler);
int CustomerID = 1234;
//Realizamos la llamada asíncrona al web service
wsProxy.getCustomerDataAsync(CustomerID);
void getCustomerDataCompletedHandler(object sender,
BusinessLogicWS.getCustomerDataCompletedEventArgs e)
//Obtenemos los datos de la respuesta del web service
ObservableCollection<SilverlightTest.BusinessLogicWS.ArrayOfAnyType>
data = e.Result;
//Hacemos algo con esos datos
for (int i=0; i<data.Count; i++)
for (int j = 0; j < data[i].Count; j++ )
Debug.WriteLine(data[i][j].ToString());
De esta forma, hemos construido una interfaz entre la aplicación Silverlight y nuestra biblioteca de clases .NET, pudiendo reutilizar el código de las bibliotecas de clases .NET y solventando los problemas de incompatibilidades entre tipos de datos.
Hasta el próximo post,
Este post es la continuación de Cuándo y cómo capturar volcados de memoria en modo Crash
La herramienta Debug Diagnostics Tool se puede descargar desde aquí, tanto la versión de 32-bit como la de 64-bit. En el momento de escribir este post, la herramienta está en la versión 1.1 y está soportada para los siguientes sistemas operativos:
· Windows 2000 Server
· Windows Server 2003
· Windows XP
Es decir, si pretendemos analizar un problema en Windows Server 2008, Windows Server 2008 R2, Windows Vista o Windows 7 es recomendable utilizar otras herramientas, aunque algunas funcionalidades de Debug Diagnostics pueden funcionar en estos sistemas operativos.
Nota
En el caso de los sistemas operativos de 64-bit, es conveniente instalar la versión de DebugDiag en función del proceso que se va a monitorizar. Es decir, si el proceso es de 32-bit, es mejor utilizar la versión de 32-bit de DebugDiag al margen de que Windows sea de 64-bit (de esta forma nos quitamos del medio la capa WOW64 que a veces dificulta la depuración).
DebugDiag nos permite capturar volcados de crash mediante las reglas de crash. Las reglas de crash de DebugDiag permiten adjuntar un depurador a cualquier proceso y configurarlo para que genere volcados en el momento que se produzca una determinada excepción o cuando se finalice el proceso. Estos son los escenarios más comunes:
Volcados de crash de finalización de un proceso
Volcados de excepciones .NET
VOLCADOS DE CRASH DE FINALIZACIÓN DE UN PROCESO
Este es el escenario más común para capturar volcados de crash, y consiste en generar un volcado de memoria en el momento que se produce la excepción no manejada que provoca la finalización del proceso.
Para configurar una regla de crash de estas características debemos posicionarnos en la pestaña [Rules] y posteriormente pulsar sobre el botón [Add Rule…]. Posteriormente seguiremos estos pasos:
1) Seleccionar la opción [Crash]
2) Configurar a que proceso(s) o aplicación(es) adjuntaremos el depurador mediante alguna de las alternativas que nos proporciona la herramienta (en muchas ocasiones hay varias opciones válidas para conseguir el mismo resultado):
a) All active IIS/COM+ related processes: Adjuntará el depurador a todos los procesos INETINFO.EXE, W3WP.EXE y DLLHOST.EXE que se ejecuten en la máquina. De esta forma obtendremos datos de cualquier crash que se produzca en un proceso relacionado con IIS.
b) A specific process: Adjuntará el depurador a un proceso específico (un determinado PID) o a todos los procesos con el mismo nombre de ejecutable (por ejemplo W3WP.EXE), en función de sí seleccionamos la opción This process instance only.
c) A specific MTS/COM+ application: Adjuntará el depurador al proceso DLLHOST.EXE que aloje la aplicación COM+ que indiquemos.
d) Web application pool: Adjuntará el depurador al proceso W3WP.EXE que aloje el application pool que indiquemos.
e) NT Service: Adjuntará el depurador al proceso que aloje al servicio de Windows que indiquemos, por ejemplo SVCHOST.EXE.
3) En la pantalla de configuración avanzada dejaremos la configuración predeterminada, de forma que no tome ninguna acción cuando se produzcan excepciones de first chance:
4) Por último debemos indicar el nombre de la regla, la ubicación donde volcará los datos generados y activarla para que empiece a capturar datos. En función del escenario, estos datos pueden ser de gran tamaño por lo que conviene elegir una unidad que no tenga problemas de espacio.
VOLCADOS DE EXCEPCIONES .NET
En muchas ocasiones necesitamos analizar volcados de determinadas excepciones .NET, para examinar el estado de los objetos implicados en el momento de producirse la excepción. A partir de .NET 2.0, el CLR hace una gran labor registrando información sobre las excepciones no manejadas en el log de eventos NT, pero en ocasiones no es suficiente y necesitamos los volcados de memoria. Para configurar una regla de crash de estas características debemos posicionarnos en la pestaña [Rules] y posteriormente pulsar sobre el botón [Add Rule…]. Posteriormente seguiremos estos pasos:
2) Configurar a que proceso(s) o aplicación(es) adjuntaremos el depurador mediante alguna de las alternativas que nos proporciona la herramienta. Habitualmente nos adjuntaremos a uno o varios procesos W3WP.EXE mediante alguno de los mecanismo que nos proporciona la herramienta:
a. All active IIS/COM+ related processes: Adjuntará el depurador a todos los procesos INETINFO.EXE, W3WP.EXE y DLLHOST.EXE que se ejecuten en la máquina. De esta forma obtendremos datos de cualquier crash que se produzca en un proceso relacionado con IIS.
b. A specific process: Adjuntará el depurador a un proceso específico (un determinado PID) o a todos los procesos con el mismo nombre de ejecutable (por ejemplo W3WP.EXE), en función de sí seleccionamos la opción This process instance only.
c. A specific MTS/COM+ application: Adjuntará el depurador al proceso DLLHOST.EXE que aloje la aplicación COM+ que indiquemos.
d. Web application pool: Adjuntará el depurador al proceso W3WP.EXE que aloje el application pool que indiquemos.
e. NT Service: Adjuntará el depurador al proceso que aloje al servicio de Windows que indiquemos, por ejemplo SVCHOST.EXE.
3) En la pantalla de configuración avanzada pulsamos sobre el botón [Exceptions] y en la ventana de First Chance Exception Configuration le damos a [Add Exception…]. Esto abre una nueva ventana donde configuraremos la excepción específica que queremos monitorizar. Debemos seleccionar el tipo de excepción adecuado en la columna izquierda CLR (.NET) Exception y posteriormente indicar el tipo de excepción y la acción a tomar. Es importante tener en cuenta que el campo [.NET Exception Type] es case sensitive, por lo que debemos poner el nombre exacto de la excepción con las mayúsculas y minúsculas adecuadas. La acción a realizar cuando se produzca una excepción como la configurada será generar un Full Userdump, y limitaremos esta acción a un máximo de 10 volcados:
4) Una vez configurada la excepción adecuada, pulsamos [OK] à [Save & Close] à [Next], y finalizamos indicando el nombre de la regla, la ubicación donde volcará los datos generados y activando la regla para que empiece a capturar datos
En una serie de próximos posts, cubriré algunas técnicas para introducirse en el análisis de volcados de memoria.
Happy hacking,
En ocasiones, necesitamos capturar trazas de red durante un periodo de tiempo prolongado, por ejemplo para esperar hasta que se produzca un determinado comportamiento aleatorio que pretendemos investigar. Para estos escenarios puede ser útil capturar una traza de red circular, de forma que únicamente se guarden los últimos n MB capturados, y los datos anteriores se vayan desechando automáticamente.
De esta forma, la traza puede estar capturando datos durante días, y en el momento que se produzca el problema, paramos la traza que contendrá el tráfico de red relevante que pretendíamos investigar en un fichero de un tamaño manejable.
Es importante configurar un tamaño de buffer lo suficientemente grande para tener un margen desde que se reproduce el problema hasta que se para la traza, de forma que esta contenga el periodo de tiempo que se pretende investigar. El problema de las trazas circulares es que si se para la traza demasiado tarde, es posible que los datos que se pretendían investigar ya hayan sido desechados.
Network Monitor, permite capturar trazas circulares mediante la utilidad de línea de comandos nmcap.exe. La sintaxis para capturar una traza circular de 200 MB sería la siguiente:
C:\...\Microsoft Network Monitor 3>nmcap.exe /network * /capture /file netTrace.cap:200M
Netmon Command Line Capture (nmcap) 3.3.1641.0
Saving info to:
C:\Program Files\Microsoft Network Monitor 3\netTrace.cap - using circular buffer of size 200.00 MB.
ATTENTION: Conversations Enabled: consumes more memory (see Help for details)
Exit by Ctrl+C
Capturing | Received: 411 Pending: 0 Saved: 411 Dropped: 0 | Time: 17 seconds.
En el momento que se haya producido el problema, finalizamos la captura presionando Ctrl + C.
(1f68.140c): CLR exception - code e0434f4d (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
KERNELBASE!RaiseException+0x39:
000007fe`fcfcaa7d 4881c4c8000000 add rsp,0C8h
Examinando los detalles de la excepción .NET en cuestión (CLR Exception), podríamos ver que el depurador está capturando la siguiente excepción System.Web.HttpException:
0:032> !printexception
Exception object: 0000000155a173b8
Exception type: System.Web.HttpException
Message: The file '/myApp/myUserControl.ascx' does not exist.
InnerException: <none>
StackTrace (generated):
<none>
StackTraceString: <none>
HResult: 80004005
En esta ocasión, la aplicación ASP.NET maneja la excepción adecuadamente, y el problema no pasa a mayores. No obstante, se puede dar la circunstancia de que la aplicación no maneje la excepción, o no pueda manejarla, en cuyo caso se lanzaría una excepción de second chance. Las excepciones de second chance son la segunda y última oportunidad de capturar una excepción. En la mayoría de los casos, cuando se produce una excepción de second chance, la ejecución el proceso finaliza inmediatamente, se produce un crash.
En el siguiente ejemplo, vemos como se produce una excepción de Stack Overflow, y al no ser manejada por la aplicación (las excepciones de Stack Overflow particularmente, no se pueden manejar) se lanza primero una excepción de first chance, y posteriormente en la misma instrucción de ensamblador, se lanza la excepción de second chance. que provoca la finalización del proceso:
(1f60.1c0c): Stack overflow - code c00000fd (first chance)
eax=00032000 ebx=7ffd9000 ecx=0001f208 edx=7c82860c esi=00000000 edi=00000000
eip=00401237 esp=000378b4 ebp=000378bc iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206
StackOverflow!_chkstk+0x27:
00401237 8500 test dword ptr [eax],eax ds:0023:00032000=00000000
(1f60.1c0c): Stack overflow - code c00000fd (!!! second chance !!!)
Dependiendo del tipo de escenarios que estemos investigando, puede ser relevante analizar las excepciones de first chance, las de second chance, o las dos.
Habitualmente, cuando adjuntamos un depurador en modo crash a un proceso, queremos que el depurador que realice una determinada acción cuando se produzca un determinado evento. Básicamente hay dos escenario típicos en los que adjuntaríamos un depurador en modo crash. Para generar volcados de memoria cuando se produzca una determinada excepción de first chance, o para generar volcados de memoria cuando se finalice un proceso inesperadamente al producirse una excepción de second chance. Para más información sobre tipos de excepciones, leer el post ¿Qué son las excepciones de first chance y second chance?
Podemos utilizar dos herramientas distintas para capturar este tipo de volcados, Debug Diagnostics o Adplus. Las herramientas son funcionalmente equivalentes, aunque cada una tiene sus ventajas y sus desventajas. En los siguientes posts encontraréis la información detallada sobre cómo generar los volcados con estas herramientas:
· Cómo capturar volcados de crash con Debug Diagnostics Tool
· Cómo capturar volcados de crash con Adplus (próximamente)
Hasta pronto,
En algunos entornos, cuando se utiliza Debug Diagnostics para monitorizar excepciones de .NET, se dispara el consumo de memoria del proceso DbgHost.exe. El motivo de la pérdida de memoria es un bug en la extensión del depurador SOS.dll que se instala con el Framework .NET, concretamente al ejecutar el comando !DumpObj de dicha extensión.
Para solucionar este problema, podemos modificar el fichero de script que genera DebugDiag de forma que no utilice el comando !DumpObj sino !PrintException que no adolece del mismo problema. Por lo tanto, los pasos a seguir son los siguientes:
1) Borrar todas las reglas de DebugDiag existentes.
2) Volver a crear las regla para monitorizar excepciones .NET, pero sin activarla al final del proceso.
3) Ir a la carpeta de scripts de DebugDiag, la ruta podría ser algo parecido a esto: C:\Program Files\DebugDiag\scripts
4) En esa carpeta encontraréis algún fichero cuyo nombre empiece por CrashRule_...*.vbs con el nombre de la regla que habéis creado. Abrid este fichero con algún editor.
5) Buscad la función GetCLRExceptionType, que tendrá un aspecto parecido a este:
Function GetCLRExceptionType(ByVal ExceptionObjHexAddr, ByVal bInnerException)
Dim Output, Lines, i
If Debugger.IsClrExtensionMissing Then
WriteToLog "Unable to determine CLR exception type - extension dll could" &
"not be loaded."
Else
Output = Debugger.Execute("!DumpObj " & ExceptionObjHexAddr)
Lines = Split(Output, Chr(10))
For i = 0 To UBound(Lines)
If bInnerException Then
If Len(Lines(i) >= 68) Then
If InStr(Lines(i), "_innerException") <> 0 Then
GetCLRExceptionType = GetCLRExceptionType("0x" & Mid(Lines(i), _
60, 8), False)
Exit For
End If
ElseIf Len(Lines(i)) >= 7 Then
If InStr(Lines(i), "Name: ") = 1 Then
GetCLRExceptionType = Mid(Lines(i), 7)
Next
End Function
6) Eliminad dicha función y sustituidla por esta otra:
Output = Debugger.Execute("!PrintException")
Tokens = Split(Lines(i), " ")
For j = 0 To UBound(Tokens)
If Len(Tokens(j)) = 8 Then
GetCLRExceptionType = GetCLRExceptionType(Tokens(j), False)
If InStr(Lines(i), "Exception type:") = 1 Then
GetCLRExceptionType = Mid(Lines(i), 17)
WriteToLog(Lines(i))
7) Guardad los cambios en el fichero CrashRule_...*.vbs volved a DebugDiag y en la pestaña Rules y activad la regla.
Realizando estos pases resolveréis el problema de consumo de memoria en DbgHost.exe.
Hasta el próximo post.
Esta es una problemática relativamente común. Por ejemplo, actualizamos un ensamblado (DLL) de nuestra aplicación ASP.NET, y cuando la probamos vemos que todavía no se han reflejado los cambios y que la aplicación se comporta igual que antes del cambio. ¿Por qué no se reflejan los cambios inmediatamente?
ASP.NET trabaja con application domains para proporcionar aislamiento entre aplicaciones alojadas en un mismo proceso. El application domain contiene todos los datos relacionados con una aplicación en particular, que son (simplificando): todos los ensamblados específicos de la aplicación, un objeto HttpRuntime y un objeto Cache. Un application domain se puede descargar del proceso que lo aloja, pero un ensamblado no se puede descargar del application domain que lo ha cargado.
Por lo tanto, cuando el runtime de .NET (CLR) detecta, por ejemplo, que algún ensamblado de la carpeta ~/bin de la aplicación ha sido modificado, carga un nuevo application domain con la última versión del ensamblado, y el application domain antiguo se descarga del proceso en cuanto haya terminado de servir todas las peticiones en curso.
Cada vez que se reinicia el application domain, todos los ensamblados tienen que volver a cargarse (previamente haciendo el shadow copy), el código vuelve a compilarse por el JIT-compiler, y la cache y las variables de sesión se reinician. El proceso de reinicio del application domain es bastante costoso, por lo que conviene evitar que se produzca cuando la aplicación web tiene mucha carga de usuarios.
ASP.NET utiliza la API Win32 ReadDirectoryChangesW para monitorizar cambios en carpetas y ficheros. Cuando detecta alguno de los siguientes cambios, se reinicia el application domain (esta lista la he sacado directamente del siguiente post de mi compañera Tess):
· Modificaciones en Web.Config, Machine.Config o Glabal.asax
· Modificaciones en la carpeta ~/bin o en su contenido
· La ruta física del directorio virtual es modificada
· Alguna subcarpeta de la aplicación web es eliminada (sólo en ASP.NET 2.0)
Adicionalmente, los siguientes eventos también provocan un reinicio del application domain:
· La política CAS (Code Access Security) es modificada.
· El número de recompilaciones dinámicas1 de ficheros *.aspx, *.ascx o *.asax excede el límite especificado en la propiedad <compilation numRecompilesBeforeAppRestart="15"/> en el machine.config o web.config (por defecto este valor es de 15)
1) Es importante puntualizar que modificar un fichero *.aspx, *.ascx o *.asax también implica una recompilación, dado que en ASP.NET absolutamente todo el código termina siendo compilado, incluso el código “HTML” de la capa de presentación. Las recompilaciones dinámicas, se gestionan de una forma distinta ya que en lugar de reiniciar el application domain directamente, se invalida el ensamblado con la versión antigua del fichero .aspx, y se carga un nuevo ensamblado con la nueva versión.
A la larga, dado que no se pueden descargar ensamblados de un application domain, este comportamiento de ASP.NET supondría que el consumo de memoria aumentaría progresivamente debido a la acumulación de ensamblados invalidados. Por este motivo, ASP.NET reinicia el application domain llegado un número determinado de recompilaciones, para de esta forma poder descargar los ensamblados invalidados.
Por lo tanto, para evitar los problemas asociados a los reinicios de los application domains y a las recompilaciones dinámicas, debemos seguir las siguientes medidas:
a) Realizar despliegues a producción cuando el servidor tenga la menor carga posible, o mejor incluso sacándolo del balanceo de forma que no atienda peticiones.
b) Cuando el punto a) no sea posible, podemos añadir / modificar los parámetros waitChangeNotification y maxWaitChangeNotification para minimizar el número de reinicios del application domain. Estos parámetros se pueden configurar en el web.config de la aplicación web en cuestión. Por ejemplo:
<httpRuntime waitChangeNotification="10" maxWaitChangeNotification="60" />
Este mecanismo funciona de la siguiente manera: en el momento que sobrescribimos un fichero existente dentro de la estructura de carpetas de nuestra aplicación web, un objeto de File Change Notification detecta el cambio y transcurrido el tiempo que hayamos configurado en el campo waitChangeNotification reiniciará el application domain. Si durante este periodo de espera detecta otro cambio adicional, volverá a esperar otra vez el número de segundos que hayamos configurado en waitChangeNotification, y así sucesivamente hasta que se alcance el tiempo de espera que hayamos configurado en maxWaitChangeNotification. Cuando se alcanza el tiempo de espera configurado en maxWaitChangeNotification, el application domain se reinicia al margen de que se sigan detectando cambios. De esta forma, el application domain no se reinicia hasta que se hayan “terminado de copiar” todos los nuevos ficheros a la carpeta de contenido, evitando así múltiples reinicios con su consiguiente coste. Esta problemática también está descrita en detalle en el post al que hacía referencia antes de mi compañera Tess Ferrandez.
c) En ocasiones es interesante probar si deshabilitando la “compilación por lotes” mejora el comportamiento. Este cambio ocasiona que no se compile la aplicación entera cuando se carga el application domain sino únicamente las partes relevantes de la aplicación requeridas para atender la primera petición. Por un lado se reduce el retraso inicial para responder a la primera petición, aunque por otro lado, cuando llegue una petición posterior que requiera una parte de la aplicación que no haya sido compilada, esa petición también experimentará un retraso. Este cambio aplica sobre todo para aplicaciones compuestas por gran cantidad de ensamblados y páginas.
<compilation batch="false" />
Espero que esta información os sea de utilidad.
Este post es la continuación de Cuándo y cómo capturar volcados de memoria en modo Hang
DebugDiag nos permite capturar volcados de hang básicamente de tres formas distintas:
Volcado manual de todos los procesos de IIS Volcado manual de un proceso específico Regla de hang
Volcado manual de todos los procesos de IIS
Volcado manual de un proceso específico
Regla de hang
!---->VOLCADO MANUAL DE TODOS LOS PROCESOS DE IIS
Podemos generar los volcados manualmente cuando se esté reproduciendo el problema desde el menú [Tools] à [Create IIS/COM+ Hang Dump].
Por defecto, los volcados se generarán en la ruta: C:\Program Files\DebugDiag\Logs\Misc
!---->VOLCADO MANUAL DE UN PROCESOS ESPECÍFICO
Podemos generar volcados manualmente de un proceso específico desde la pestaña [Processes] à [Botón derecho sobre el proceso] à [Create Full Userdump]
Al igual que en el anterior caso, los volcados se generan en la ruta: C:\Program Files\DebugDiag\Logs\Misc.
!---->REGLA DE HANG
Las reglas de hang de Debug Diagnostic permiten monitorizar la salud de una aplicación web de forma automatizada, y generar volcados en el momento que la herramienta detecta que la aplicación no respohnde en los tiempos establecidos.
Para configurar una regla de hang debemos posicionarnos en la pestaña [Rules] y posteriormente pulsar sobre el botón [Add Rule…]. Posteriormente seguiremos estos pasos:
1) Seleccionar la opción [IIS Hang]
2) En la siguiente pantalla, seleccionar las URLs que queremos monitorizar con la herramienta. Debemos configurar al menos una URL, pero en algunos escenario puede tener sentido monitorizar varias. El funcionamiento del mecanismo de monitorización es muy sencillo. Se basa en realizar peticiones HTTP a las URLs configuradas, y esperar hasta que una de esas peticiones no sea atendida en el tiempo establecido para alertar al depurador que se está produciendo un hang. Para cada URL, configuraremos independientemente la propiedad ping interval, que es el intervalo de tiempo entre cada ping a la URL, y la propiedad timeout que es el intervalo de tiempo que vamos a esperar a recibir una respuesta. En el momento que la respuesta al ping exceda el tiempo que hemos especificado en timeout, se generará un volcado automáticamente. Los valores predeterminados de la herramienta establecen el valor de la propiedad timeout a 120 segundos. Personalmente opino que es un valor muy excesivo, y que en la mayoría de los casos, si una petición HTTP no ha respondido en 30 segundos, se puede confirmar que se está produciendo una contención. Es difícil hacer una recomendación genérica sobre estos parámetros, porque depende mucho de cada aplicación y cada escenario individual. Yo suelo recomendar poner ambos valores a 30, pero al final eso lo tiene que decidir quien conoce la aplicación.
3) En el siguiente paso, debemos configurar que hará la herramienta cuando detecte que se está produciendo una situación de hang. Para ello debemos pulsar sobre el botón [Add Dump Target] y seleccionar alguna de las siguiente opciones:
a) All active IIS/COM+ related processes: Generará volcados de todos los procesos INETINFO.EXE, W3WP.EXE y DLLHOST.EXE. Esta opción es interesante cuando no sabemos a priori cual es el proceso que provoca la contención.
b) Executable: Generará volcados de todos los procesos con el mismo nombre que el proceso que indiquemos, por ejemplo W3WP.EXE.
c) COM+ application: Generará un volcado del proceso DLLHOST.EXE que aloje la aplicación COM+ que indiquemos.
d) Web application pool: Generará un volcado de memoria del proceso W3WP.EXE que aloja el application pool que indiquemos.
e) NT Service: Generará un volcado del proceso que aloje al servicio de Windows que indiquemos, por ejemplo SVCHOST.EXE.
4) Por último debemos indicar el nombre de la regla y la ubicación donde volcará los datos generados. En función de la circunstancias, estos datos pueden ser de gran tamaño por lo que conviene elegir una unidad que no tenga problemas de espacio.
En el contexto de este post, se entiende por hang (yo lo traduciría al castellano como contención) una situación en la que una aplicación responde mucho más despacio de lo normal o deja de responder por completo. Una contención puede ser temporal (pasado un rato la aplicación vuelve a funcionar correctamente) o permanente (lo que se conoce como deadlock), y puede ir asociada a un consumo de CPU elevado o un consumo bajo (o incluso nulo).
Los volcados de memoria en modo hang muestran el estado de la memoria de un proceso en un momento determinado en el tiempo. Por lo tanto, para que la información contenida en el volcado tenga algún valor es imprescindible capturar el volcado en el momento adecuado. Revisemos los distintos escenarios en los que necesitaremos capturar volcados de memoria en modo hang:
Contención y consumo de CPU bajo
Habitualmente, cuando la aplicación no responde ni consume CPU es que está esperando a algo que se ejecuta en un proceso distinto o incluso en un servidor distinto. Por ejemplo, puede estar esperando a obtener respuesta de una llamada RPC, a obtener respuesta de una llamada por sockets, o a que se libere una sección crítica. En ocasiones, como ya adelantaba antes, estas contenciones pueden ser permanentes si esa respuesta nunca llega o la sección crítica nunca se libera (siguiendo con el ejemplo anterior).
Para este tipo de problemas es interesante capturar tres volcados con un intervalo de un minuto entre cada uno (aproximadamente), y cuando sea posible capturar volcados de todos los procesos implicados en la aplicación cuando se esté reproduciendo el problema.
Esto nos permitirá ver cómo evoluciona el proceso con el tiempo (por eso cogemos varios volcados), y además obtendremos datos de todos los procesos implicados dado que a priori no podemos saber en qué proceso se produce la contención.
Las dos herramientas más comunes para capturar estos volcados son Debug Diagnostics y Adplus. En los siguientes posts encontraréis la información detallada sobre cómo generar los volcados:
· Cómo capturar volcados de hang con Debug Diagnostics Tool
· Cómo capturar volcados de hang con Adplus (próximamente)
Contención y consumo de CPU elevado
Para este tipo de problemas, al contrario que la categoría anterior, es más evidente cual es el proceso que nos interesa analizar (el que consume CPU). Por lo tanto, la forma de capturar los volcados es ligeramente distinta.
Para este tipo de problemas es interesante capturar tres volcados con un intervalo de un minuto entre cada uno (aproximadamente) del proceso específico con elevado consumo de CPU cuando se esté reproduciendo el problema.
Al igual que en el anterior escenario, podemos utilizar las herramientas Debug Diagnostics y Adplus para capturar este tipo de volcados. Adicionalmente, existe una herramienta llamada ProcDump diseñada especialmente para generar volcados cuando un proceso supere un determinado umbral de consumo de CPU. En los siguientes posts encontraréis la información detallada sobre cómo generar los volcados:
· Cómo capturar volcados de hang con ProcDump (próximamente)
Próximamente veremos cuándo y cómo capturar volcados de memoria en modo crash.
Hasta pronto
Imaginemos el siguiente escenario: Tenéis una aplicación web que requiere o acepta certificados de cliente, y que mediante un formulario HTML hace POST para subir ficheros al servidor. En algunas ocasiones, cuando los ficheros superan un determinado tamaño, la petición falla y en los logs de IIS vemos un error HTTP 413 – Request entity too large.
Este es un comportamiento conocido cuando están habilitados certificados de cliente en combinación con que se realiza un petición HTTP de gran tamaño (por ejemplo un POST HTTP adjuntando ficheros). El motivo es que IIS lee los primeros n bytes de la petición (ahora veremos qué determina esa n), asumiendo que deberían haber llegado todos los encabezados de la petición (HTTP headers), pero no necesariamente todos los datos asociados (entity body) de la petición.
Esto habitualmente no supone un problema, pero en nuestro escenario en particular, IIS examina los encabezados de la petición, encuentra que la página requiere certificados de cliente y que por lo tanto necesita renegociar la conexión SSL. Desafortunadamente, el cliente (IE) no puede renegociar la conexión porqué está esperando a poder enviar el resto de datos del entity body a IIS. Para evitar que se produzca un deadlock (IIS esperando a poder renegociar la conexión SSL e IE esperando a poder mandar el entity body), IIS devuelve un error HTTP 413 y cierra la conexión.
Para evitar que se produzca este problema tenemos dos opciones:
1) Para que la renegociación de la conexión se pueda llevar a cabo, el entity body completo tiene que estar “precargado” utilizando SSL Preload. SSL Preload utiliza la propiedad de la metabase UploadReadAheadSize para determinar el tamaño del buffer en el que almacenará la petición (los n bytes a los que hacía referencia antes). Por lo tanto si el tamaño de este buffer es inferior al tamaño total de la petición HTTP (lo que incluye los encabezados HTTP y el entity body), IIS devuelve el error HTTP 413.
El tamaño predeterminado de ese buffer son 48k, lo cual es un tamaño insuficiente en muchas ocasiones, pero que fue establecido así para evitar ataques de denegación de servicio subiendo ficheros “basura” de gran tamaño.
La sintaxis para aumentar dicho buffer es la siguiente, y se puede especificar de forma granular a nivel de directorio virtual:
C:\inetpub\adminscripts>cscript adsutil.vbs set w3svc/1/root/myApp/uploadreadaheadsize 20000000
Microsoft (R) Windows Script Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.
uploadreadaheadsize : (INTEGER) 20000000
El número indicado es el número de bytes, por lo que 20.000.000 bytes (como en el ejemplo) serían aproximadamente unas 19,1MB.
2) La otra alternativa es habilitar la clave de la metabase SSLAlwaysNegoClientCert de forma que forcemos a que por cada nueva conexión SSL que se establezca, el servidor negociará inmediatamente el certificado de cliente. De esta forman se previene la renegociación, lo cual supone una ganancia de rendimiento en sí misma, y además evita el problema asociado a las peticiones con entity body de gran tamaño. La sintaxis para habilitar esta clave es:
C:\inetpub\adminscripts>cscript adsutil.vbs set w3svc/1/SSLAlwaysNegoClientCert true
SSLAlwaysNegoClientCert : (BOOLEAN) True
INFORMACIÓN ADICIONAL
Client cannot renegotiate request and returns an HTTP 413 error (IIS 6.0)
http://www.microsoft.com/technet/prodtechnol/WindowsServer2003/Library/IIS/7e0d74d3-ca01-4d36-8ac7-6b2ca03fd383.mspx?mfr=true
Happy hacking
Los ordenadores procesan texto utilizando tablas de codificación para convertir secuencias de bits en caracteres alfanuméricos y viceversa. Cuando desarrollamos aplicaciones, y muy especialmente aplicaciones distribuidas o aplicaciones web, tenemos que tener en cuenta la codificación (o enconding) con la que “ciframos” y “desciframos” los mensajes, dado que si utilizamos codificaciones distintas el mensaje resultante puede resultar incorrecto.
Antes de meternos de lleno en las consideraciones específicas de desarrollo, revisemos brevemente los encodings más comunes que puede ser interesante conocer.
ASCII
ASCII (American Standard Code for Information Interchange) se desarrolló en los años 60 y es una tabla de codificación que utiliza 7 bits para representar 128 caracteres, de los cuales 94 son caracteres legibles y 33 son caracteres de control y el espacio que se considera un carácter invisible. Esta codificación no sirve para representar la ‘ñ’ o los acentos, y hoy en día en muchos contextos se considera obsoleta.
Windows-1252 (Western European)
Históricamente era la codificación predeterminada en muchos contextos de Windows. Es una codificación de 8 bits para representar 256 caracteres, e incluye la mayoría de los caracteres utilizados en los alfabetos de Europa occidental.
ISO-8859-1 (Latin 1)
Esta codificación ISO de 8 bits fue publicada en 1985, y es un subconjunto de la codificación Windows-1252 compuesto por 191 caracteres. Es informalmente conocida como Latin 1.
ISO-8859-15 (Latin 9)
En 1999 se publicó esta codificación como una actualización de ISO-8859-1 para corregir algunas de sus limitaciones. Sigue utilizando 8 bits para representar cada carácter, y entre otros cambios se añadieron algunos caracteres del alfabeto fines (Š y Ž), y el símbolo del euro (€). Para poder acomodar estos cambios, se tuvieron que eliminar algunos otros caracteres de uso poco frecuente, entre los cuales se encontraban: ¤, ¦, ¨, ´, ¼, ½, y ¾.
UTF-8
UTF-8 (8-bit Unicode Transformation Format) es una codificación de longitud variable entre 1 y 4 bytes. La representación de un byte (8 bits) de UTF-8 se reserva exclusivamente a los 128 caracteres de la tabla ASCII lo cual lo hace compatible hacia atrás. Los siguientes 1920 caracteres de UTF-8 requieren dos bytes para ser codificados, entre los cuales se incluyen el alfabeto latino, griego, cirílico, hebreo, árabe, sirio, etc.
Por lo tanto, la moraleja de este breve resumen, es que aunque tanto Windows-1252 y UTF-8 sean capaces de representar el carácter ‘Ñ’, lo hacen de forma distinta. De hecho, para ser más precisos lo hacen así:
Codificación
Glifo
Representación Binaria
Representación Hexadecimal
Windows-1252
Ñ
1101001
D1
1100011 10010001
C3 91
Por lo tanto, queda claro que si codificamos un texto utilizando Windows-1252 y posteriormente lo decodificamos los bytes resultantes utilizando UTF-8, lo que inicialmente era un ‘Ñ’ va a pasar a ser cualquier otra cosa excepto una ‘Ñ’ al decodificarlo.
Ahora que ya tenemos más clara la problemática, os detallo algunas consideraciones concretas para ASP.NET:
1) Dado que ASP.NET nos permite configurar la codificación utilizada para generar las respuestas, y la codificación predeterminada para decodificar las peticiones, es importante asegurarse de que se utiliza la misma codificación en la otra parte implicada, el cliente. Esta es la configuración predeterminada de ASP.NET:
<configuration> <system.web> <globalization
requestEncoding="utf-8"
responseEncoding="utf-8" /> </system.web></configuration>
2) En una respuesta HTTP podemos especificar la codificación del contenido en dos sitios. En el encabezado HTTP Content-Type o en un meta tag dentro del propio documento HTML. En caso de conflicto entre ambos (como en el ejemplo), tiene preferencia la configuración especificada en el encabezado HTTP.
HTTP/1.1 200 OK
Date: Tue, 1 Dec 2009 10:23:34 GMT
Server: Microsoft-IIS/6.0
Content-Length: 48
Content-Type: text/html; charset=utf-8
Cache-control: private
<html xmlns="http://www.w3.org/1999/xhtml">
<head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" /> </head> <body> … </body>
</html>
Un problema frecuente es enviar respuestas HTTP sin especificar en ningún sitio la codificación utilizada. Cuando se da el caso, el cliente que recibe la respuesta tiene que determinar la codificación que utilizará para decodificar el contenido. Habitualmente, es tan sencillo como que el cliente utilizará la codificación predeterminada del sistema operativo, por lo que existe un elevado riesgo de que en algún cliente con una determinada configuración se muestre incorrectamente.
3) Otro problema con el que me he encontrado en varias ocasiones es relativo al dialogo de descarga de Internet Explorer. Cuando generamos una respuesta HTTP dinamicamente desde ASP.NET para enviar un fichero, podemos encontrarnos con que el dialogo de descarga muestra el nombre del fichero con caracteres incorrectos:
Este problema se debe a que ASP.NET (1.1 y 2.0) por defecto codifican los encabezados HTTP con UTF-8. En ASP.NET 1.1 es necesario instalar la siguiente actualización para poder especificar la codificación de los encabezados:
FIX: The response header will always be encoded as UTF-8 when you use the Response.Addheader method in ASP.NET
http://support.microsoft.com/default.aspx?scid=kb;EN-US;895262
Una vez instalada la actualización, podemos especificarla en el web.config de la aplicación:
<globalization responseHeaderEncoding="Windows-1252"/>
En ASP.NET 2.0 se puede cambiar la codificación de los encabezados HTTP programáticamente además de mediante la opción del web.config (igual que en ASP.NET 1.1):
protected void Page_Load(object sender, EventArgs e)
//Leemos el fichero DOCX y lo almacenamos en un array de bytes
string FileName = "Diseño gráfico.docx";
string FilePath = MapPath(FileName);
byte[] FileBytes = File.ReadAllBytes(FilePath);
//ASP.NET codifica los encabezados HTTP con 'utf-8' de forma
//predeterminada. Especificamos que utilice la codificación
//ANSI predeterminada del OS, que en mi caso es 'Windows-1252'
Response.HeaderEncoding = System.Text.Encoding.Default;
//Establecemos el MIME type de la respuesta y añadimos el
//encabezado HTTP 'Content-Disposition'
Response.ContentType = "application/docx; charset=utf-8";
Response.AddHeader("Content-Disposition", "Inline; filename=" +
FileName);
//Envíamos el fichero en formato binario
Response.BinaryWrite(FileBytes);
Hasta el próximo post
La mayoría de los casos de soporte que nos abren para diagnosticar problemas al conectarse por HTTPS a un sitio web, suelen resolverse realizando alguno de los pasos descritos a continuación. Partimos de la premisa de que podemos acceder correctamente por HTTP para acotar el escenario a un problema al establecer la conexión HTTPS.
1) Lo primero que suelo comprobar es que el certificado es válido y está emitido con el propósito adecuado.
a) Para ello debemos examinar las propiedades del certificado de servidor, y en la pestaña General verificar que el certificado es válido y tiene una clave privada.
b) Comprobar también en la pestaña Details que la propiedad Enhanced Key Usage (o Uso Mejorado de Clave) incluye el valor Server Authentication (1.3.6.1.5.5.7.3.1).
c) Por último vayamos a la pestaña Certification Path y verifiquemos que todos los certificados en la cadena de certificación tienen el estado OK.Las condiciones descritas en los pasos a, b y c constituyen el mínimo imprescindible para que se pueda establecer una conexión SSL, pero el hecho de que todo esté correcto no descarta por completo que el problema se encuentre en el certificado. Más adelante realizaremos pruebas adicionales para descartar problemas de integridad con el certificado.
2) Comprobar si existe un conflicto con alguna otra aplicación que esté escuchando por el puerto 443 (o el que aplique en cada caso). Si este es el caso, es posible que veamos el siguiente evento registrado en los logs de eventos NT:
Event Type: Error
Event Source: W3SVC
Event Category: None
Event ID: 1114
Description: One of the IP/Port combinations for site '1' has already been configured to be used by another program. The other program's SSL configuration will be used.
Para diagnosticar esta situación, inicialmente ejecutaremos el siguiente comando:
C:\WINDOWS\system32>netstat -noa
Si todo es correcto, sólo debería haber una aplicación escuchando por el puerto 443, y el resultado debería ser algo parecido a esto (el PID 4 siempre equivale al proceso System):
Active Connections
Proto Local Address Foreign Address State PID
TCP 0.0.0.0:80 0.0.0.0:0 LISTENING 4
TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 688
TCP 0.0.0.0:443 0.0.0.0:0 LISTENING 4
TCP 0.0.0.0:445 0.0.0.0:0 LISTENING 4
Si nos surgen dudas sobre si realmente es IIS (o el proceso System) el que está escuchando por el puerto 443, podéis utilizar la herramienta tasklist.exe o simplemente Task Manager para confirmarlo. Si el puerto estuviera en uso por otra aplicación, habría que determinar qué aplicación usa el puerto y para qué. La forma de verificar si este es realmente el problema sería probar a configurar SSL en un puerto distinto y verificar si funciona.
Si netstat no arroja luz sobre el conflicto de puertos, pero no obstante vemos el evento mencionado anteriormente (Event ID 1114) en los logs de eventos, ejecutaremos el siguiente comando para sacar un listado de los certificados SSL asociados a IIS:
C:\Program Files\Support Tools>httpcfg query ssl
Cuando revisemos la lista debemos buscar alguna entrada en la que el campo de Hash y CertStoreName están vacíos o nulos, y el Guid está todo a ceros. Este es indicativo de un problema.
IP : 0.0.0.0:443
Hash :
Guid : {00000000-0000-0000-0000-000000000000}
CertStoreName : (null)
CertCheckMode : 0
RevocationFreshnessTime : 0
UrlRetrievalTimeout : 0
SslCtlIdentifier : (null)
SslCtlStoreName : (null)
Flags : 0
Para solventar este problema, debemos eliminar el binding entre IIS y el certificado ejecutando el siguiente comando (la sintaxis genérica es: httpcfg delete ssl -i ip:puerto):
C:\Program Files\Support Tools>httpcfg delete ssl -i 0.0.0.0:443
Y posteriormente volver a configurar el certificado SSL desde INETMGR.EXE. Si volvemos a listar los certificados SSL con httpcfg.exe el resultado ahora debería ser algo parecido a esto:
Hash : d753a06831d416db4bf2 9f5 aa33ae714dfba5d
Guid : {4dc3e181-e14b-4a21-b022-59fc669b0914}
CertStoreName : MY
SslCtlIdentifier :
SslCtlStoreName :
3) Crear un certificado de pruebas con SelfSSL.exe para verificar que el problema no es del certificado. Esta herramienta se puede descargar como parte del Resource Kit de IIS 6.0. La herramienta genera un certificado de pruebas automáticamente y lo instala para un sitio web determinado. La sintaxis es la siguiente:selfssl.exe /N:CN=[Nombre Cert.] /V:[Días Validez Cert.] /S:[ID Sitio Web] /P:[Puerto SSL]
Por ejemplo, para nuestro sitio web de anteriores ejemplos ejecutaríamos el siguiente comando y posteriormente volveríamos a probar si podemos establecer la conexión HTTPS:
C:\Program Files\IIS Resources\SelfSSL>selfssl.exe /N:CN=mywebsite.microsoft.com /V:365 /S:1 /P:443
Microsoft (R) SelfSSL Version 1.0
Copyright (C) 2003 Microsoft Corporation. All rights reserved.
Do you want to replace the SSL settings for site 1 (Y/N)?Y
The self-signed certificate was successfully assigned to site 1.
4) Por último reinstalaremos el certificado seleccionando manualmente el almacén de certificados en el que se instala. Es decir, al instalar el certificado, no seleccionar la opción de Automatically select the certificate store based on the type of certificate, sino hacerlo manualmente marcando la casilla Show physical stores y seleccionando el almacén deseado desde aquí. De esta forma descartaremos que tenemos el problema descrito en el post Certificate has private key but we get "the keyset does not exist" error.
Si llegados a este punto seguís sin poder establecer una conexión HTTPS contra vuestro servidor, será un buen momento para abrir un caso de soporte con Microsoft.
Mi compañero Alejandro Campos ha terminado recientemente una serie de posts dedicados a la depuración de aplicaciones .NET utilizando WinDbg. La serie es una referencia muy completa que explica desde cómo poner un punto de parada al ejecutar un determinado método, cómo ver que objetos consumen más memoria, cómo desensamblar un ensamblado .NET, hasta cómo ver que threads consumen más tiempo de CPU. http://blogs.msdn.com/alejacma/archive/2009/07/07/managed-debugging-with-windbg-introduction-and-index.aspx
DESCRIPCIÓN DEL PROBLEMA
Tenéis una aplicación ASP.NET que requiere certificados de cliente y en la que realizan peticiones HTTP de gran tamaño (por ejemplo un POST HTTP adjuntando ficheros). Cuando el tamaño de la petición (o el fichero adjunto) supera un determinado límite falla, y en los logs de IIS vemos el error HTTP 413 – Request entity too large. Si la aplicación cliente es .NET, la petición HTTP o llamada a web service fallará, y veremos el siguiente mensaje de error asociado:
The underlying connection was closed: An unexpected error occurred on a send.
O su variante en castellano:
Se ha terminado la conexión: Error inesperado de envío
RESOLUCIÓN
Ver el post Detalles sobre el error HTTP 413.
Hoy quería escribir sobre caso en el que trabajé hace unas semanas, no por la complejidad del problema, si no por mostrar un ejemplo de cómo abordar un problema y qué herramientas utilizar cuando no tienes ni idea de por dónde van los tiros. Los datos iniciales que tenía del problema era que el cliente tenía un portal de SharePoint en el que todas las páginas le daban errores HTTP 401 si habilitaba la autenticación de Windows integrada. Si configuraba autenticación básica o acceso anónimo los portales funcionaban correctamente.
Mi primera hipótesis fue que probablemente los errores se debían a un problema en la configuración de Kerberos, pero pronto descubrí que tenían configurada únicamente la autenticación por NTLM. Kerberos estaba deshabilitado en la configuración de IIS:
C:\Inetpub\AdminScripts>cscript adsutil.vbs get W3SVC/1/NTAuthenticationProviders
NTAuthenticationProviders : (STRING) "NTLM"
Puesto que no tenía ni idea de cuál podría ser la causa de este comportamiento, le pedí los siguientes logs al cliente:
· La metabase de IIS (en Windows Server 2003 se encuentra en la siguiente ruta: %WINDIR%\system32\inetsrv\metabase.xml).
· Los logs de IIS.
· Unas trazas de red capturadas mientras reproducían el error.
Examinando los logs de IIS veíamos los errores HTTP 401, pero no arrojaban luz sobre el origen del problema:
#Fields: date time s-sitename … sc-status sc-substatus sc-win32-status
2009-09-16 14:37:46 W3SVC1 … 401 2 2148074254
2009-09-16 14:37:46 W3SVC1 … 401 1 0
Yo esperaba encontrar alguna pista al menos a partir del estado win32, pero el estado 2148074254 (No credentials are available in the security package) en la primera petición es esperado (ver el siguiente post para más detalles), y en la segunda petición el resultado era 0 (The operation completed successfully). Y lo más extraño de todo, es que no se registraba una tercera y última petición para finalizar la secuencia de autenticación como sería lo normal (de nuevo, ver el post al que hacía referencia antes).
Analizando las trazas de red veíamos la secuencia de peticiones habitual durante el handshake NTLM, pero cuando el cliente hace la petición final autenticada (en el frame TCP 5004), en el siguiente frame TCP el cliente cierra la conexión TCP deliberadamente ([FIN, ACK]) y por tanto la respuesta del IIS nunca llega.
No. Time Source Dest. Prot. Info
---- --------------- -------- -------- ----- --------------------------------------------------
4503 16:09:05.973298 [CLIENT] [SERVER] TCP timeflies > 8010 [SYN] Seq=0 Win=65535 Len=0 MSS…
4504 16:09:05.973298 [SERVER] [CLIENT] TCP 8010 > timeflies [SYN, ACK] Seq=0 Ack=1 Win=1638…
4505 16:09:05.973298 [CLIENT] [SERVER] TCP timeflies > 8010 [ACK] Seq=1 Ack=1 Win=65535 Len…
4506 16:09:05.973298 [CLIENT] [SERVER] HTTP GET / HTTP/1.1
4507 16:09:05.983312 [SERVER] [CLIENT] TCP [TCP segment of a reassembled PDU]
4508 16:09:05.983312 [SERVER] [CLIENT] HTTP HTTP/1.1 401 Unauthorized (text/html)
4509 16:09:05.983312 [CLIENT] [SERVER] TCP timeflies > 8010 [ACK] Seq=441 Ack=1910 Win=6553…
4995 16:09:18.230923 [CLIENT] [SERVER] TCP timeflies > 8010 [FIN, ACK] Seq=441 Ack=1910 Win…
4996 16:09:18.230923 [CLIENT] [SERVER] TCP ndm-requester > 8010 [SYN] Seq=0 Win=65535 Len=0…
4997 16:09:18.230923 [SERVER] [CLIENT] TCP 8010 > timeflies [ACK] Seq=1910 Ack=442 Win=6509…
4998 16:09:18.230923 [SERVER] [CLIENT] TCP 8010 > ndm-requester [SYN, ACK] Seq=0 Ack=1 Win=…
4999 16:09:18.230923 [CLIENT] [SERVER] TCP ndm-requester > 8010 [ACK] Seq=1 Ack=1 Win=65535…
5000 16:09:18.230923 [CLIENT] [SERVER] HTTP GET / HTTP/1.1 , NTLMSSP_NEGOTIATE
5001 16:09:18.240938 [SERVER] [CLIENT] TCP [TCP segment of a reassembled PDU]
5002 16:09:18.240938 [SERVER] [CLIENT] HTTP HTTP/1.1 401 Unauthorized , NTLMSSP_CHALLENGE (t…
5003 16:09:18.240938 [CLIENT] [SERVER] TCP ndm-requester > 8010 [ACK] Seq=519 Ack=2082 Win=…
5004 16:09:18.240938 [CLIENT] [SERVER] HTTP GET / HTTP/1.1 , NTLMSSP_AUTH, User: EMEA\daniem
5005 16:09:18.240938 [CLIENT] [SERVER] TCP ndm-requester > 8010 [FIN, ACK] Seq=1221 Ack=208…
5006 16:09:18.240938 [SERVER] [CLIENT] TCP 8010 > ndm-requester [ACK] Seq=2082 Ack=1222 Win…
HTTP/1.1 401 Unauthorized\r\n
Content-Length: 1539\r\n
Content-Type: text/html\r\n
Server: Microsoft-IIS/6.0\r\n
[truncated] WWW-Authenticate: NTLM TlRMTVNTUAACAAAACAAIADgAAA…
X-Powered-By: ASP.NET\r\n
MicrosoftSharePointTeamServices: 12.0.0.6007\r\n
Date: Wed, 24 Jun 2009 16:09:18 GMT\r\n
Connection: close\r\n
Revisando de nuevo la configuración de IIS (en el fichero metabase.xml), pude confirmar que los Keep-Alives estaban deshabilitados para todo el IIS:
<IIsWebService Location="/LM/W3SVC" AllowKeepAlive="FALSE" ... />
Habilitándolo de nuevo el problema quedaba resuelto y la autenticación NTLM comenzó a funcionar correctamente.
Nota: Esta dependencia de los HTTP Keep-Alives es específica de NTLM y no de la autenticación de Windows integrada en general. La autenticación Kerberos puede funcionar al margen de que se habiliten los Keep-Alives o no.
Para más información, leer el siguiente artículo de TechNet:
401.1 and 401.2-Authentication Problems (IIS 6.0)
http://www.microsoft.com/technet/prodtechnol/WindowsServer2003/Library/IIS/8feeaa51-c634-4de3-bfdc-e922d195a45e.mspx?mfr=true
Espero que os haya sido de utilidad
Este post es la continuación de Cosas que deberías saber sobre el Garbage Collector de .NET
¿Para qué necesitamos destructores en .NET? Como vimos en el anterior post, en .NET no tenemos que preocuparnos de destruir ni de liberar el espacio de los objetos dado que el GC se ocupa de esta tarea. No obstante, con mucha frecuencia nuestras aplicaciones mantienen referencias a objetos nativos (es decir, no .NET), como por ejemplo handles a ficheros, conexiones a bases de datos, etc. El GC sólo entiende de objetos .NET, y por lo tanto queda en manos del desarrollador asegurarse de que su código limpia y destruye adecuadamente los recursos nativos de los que hace uso.
Para asegurarnos que nuestros objetos .NET limpian toda la memoria no manejada cuando su vida finalice, tenemos que hacer dos cosas. La primera es que nuestra clase implemente la interfaz IDisposable y por tanto el método Dispose(). Este método debe ocuparse de realizar las tareas de limpieza de recursos no manejados, y finalmente llamar al método SuppressFinalize, enseguida veremos porqué.
GC.SuppressFinalize(this);
Para cierto tipo de objetos, el método Dispose() se le llama con otro nombre por motivos puramente semánticos. Por ejemplo, la clase SqlConnection tiene un método Close() y un método Dispose() que son funcionalmente equivalentes. En el caso de conexiones a base de datos, para la mayoría de los desarrolladores resulta más intuitivo llamara a Close() a la hora de cerrar la conexión y liberar los recursos asociados.
En C# (si no me equivoco VB no tiene nada equivalente) existe la instrucción using que nos permite establecer un contexto en el que se va a utilizar un objeto .NET. Al salir de dicho contexto (cuando llegamos a la llave de cierre ‘}’) se llamará automáticamente al método Dispose() del objeto en cuestión. En el siguiente ejemplo, al salir del contexto llamaríamos al método Dispose() del objeto fs:
string path = @"c:\temp\MyTest.txt";
using (FileStream fs = File.Create(path))
byte[] info = new UTF8Encoding(true).GetBytes("This is some text");
fs.Write(info, 0, info.Length);
El problema de la interfaz IDisposable es que dependemos de que los desarrolladores que consuman nuestras clases se acuerden de llamar a los métodos Dispose() o Close(), porque de lo contrario todo el esfuerzo habrá sido inútil. En previsión de que a los desarrolladores a veces se les pueda olvidar deshacerse correctamente de sus objetos, existen los destructores (en C#) o método Finalize() (en VB). En adelante me referiré a ambos cómo método Finalize(), pero son equivalentes y simplemente tienen nombres distintos por motivos lingüísticos.
Por lo tanto, la segunda de las cosas que debemos hacer es precisamente implementar un método Finalize(). El método Finalize() es un mecanismo que nos proporciona .NET para ejecutar código de finalización de un objeto de forma automática, antes de que el GC libere su memoria.
Uno de los principios básicos de .NET es que el código del método Finalize() de una clase debe ser muy rápido, nunca puede lanzar una excepción ni tampoco bloquear la ejecución. Los métodos Finalize() de todos los objetos de un proceso se ejecutan en un mismo thread conocido como Finalizer. Cuando el GC determina que los objetos están listos para ser finalizados los mete en una cola y dicho thread ejecuta los métodos Finalize() de cada objeto secuencialmente. Hasta que no termina la ejecución del método Finalize() de un objeto, no comienza con el siguiente. Por lo tanto si se produce un bloqueo durante la finalización de alguno de los objetos, todo lo que venga detrás nunca será finalizado con todas las consecuencias que ello conlleva. Adicionalmente, en ASP.NET, este thread es el único que no tiene un manejador de excepciones predeterminado, por lo que cualquier excepción en este thread provocaría la finalización inmediata del proceso (también conocido como crash).
Para maximizar el rendimiento de nuestras aplicaciones .NET, debemos procurar que la menor cantidad posible de objetos sean finalizados por el GC. En el mejor de los casos, el método Finalize() no sería llamado nunca, sino que todos los objetos serían finalizados mediante la interfaz IDisposable. Para que os hagáis una idea de la penalización para el rendimiento que supone que un objeto sea finalizado por el GC, observad las diferencias en la tabla a continuación:
Finalización mediante Dispose()
Finalización mediante Finalize()
Vida útil del objeto
Al crearse un nuevo objeto .NET, el CLR comprueba si este tiene un método Finalize(). Si es así, añade una referencia a dicho objeto en una estructura manejada por el GC llamada la cola de Finalización. Esta cola contiene todos los objetos que hay que finalizar, antes de que el GC libere su memoria.
Cuando finaliza la vida útil del objeto, desde el código de la aplicación se llama su método Dispose(). En este método se realizan las tareas de limpieza de recursos no manejados, y finalmente se llama al método SuppressFinalize lo cuál elimina el objeto de la cola de Finalización.
Cuando finaliza la vida útil del objeto, desde el código de la aplicación no se llama su método Dispose().
Primera colección del GC
Cuando se desencadena una colección de basura, el GC recorre la memoria y determina qué objetos ya no están en uso y pueden ser colectados. Adicionalmente comprueba si estos objetos están referenciados en la cola de Finalización.
Si el objeto no necesita ser finalizado, su memoria es liberada.
Los objetos que se encuentran en la cola de Finalización son eliminados de esta y movidos a una nueva cola llamada Freachable. La cola Freachable contiene los objetos que están listos para que su método Finalize() sea llamado. Es decir, son basura pero todavía no han sido finalizados. Esta cola actúa como un objeto raíz e impide que los objetos a los que referencia sean colectados.
El objeto sobrevive la colección de basura y promociona a la siguiente generación (a no ser que ya estuviera en la generación 2).
Tiempo transcurrido entre colecciones
El thread del Finalizer comienza a llamar a los métodos Finalize() de todos los objetos que se encuentran en la cola Freachable. Una vez ha finalizado la llamada al método Finalize(), los objetos son eliminados de esta cola. Cuando la cola Freachable se vacía, el thread del Finalizer pasa en un estado de espera.
Segunda colección del GC
Dado que el objeto ya no se encuentra referenciado ni por la cola de Finalización ni por la cola Freachable, su memoria es liberada.
En definitiva, necesitamos implementar un destructor para nuestras clases, pero debemos procurar que dicho destructor no sea utilizado nunca. Espero que os haya convencido.
- Hasta el próximo post,
Daniel Mossberg
La mayoría de los desarrolladores de .NET tenemos algunas nociones básicas sobre que es el Garbage Collector (en adelante GC) y para qué sirve. No obstante, con frecuencia pasamos por alto algunos detalles de su funcionamiento interno que provocan que nuestro código no sea todo lo eficiente y escalable que debería.
El objetivo del GC es proporcionar una capa de abstracción para los desarrolladores en cuestiones de manejo de memoria. Esto introduce una gran ventaja sobre otros lenguajes de programación en los que el desarrollador se tiene ocupar por completo de esta tarea. Escribir código que maneje correctamente su memoria en todas las situaciones no es ni mucho menos trivial, y las posibilidades de introducir bugs en la aplicación son múltiples: corrupción del heap, corrupción del stack, pérdida de memoria, fragmentación de memoria etc.
Si bien el GC simplifica la tarea de manejo de memoria para los desarrolladores, no les exime por completo. Para hacer buen uso de la memoria en .NET es importante conocer como la maneja el GC internamente. Vayamos por partes.
El GC de .NET es un colector de basura generacional. Esto significa que clasifica los objetos en distintas generaciones, lo cual le permite realizar colecciones de basura parciales (de una o varias generaciones) y así evitar hacer siempre colecciones de basura completas de todo el heap de .NET. Esta característica es una de las más importantes en cuanto al rendimiento del GC, y permiten que el GC de .NET sea escalable para aplicaciones de alta concurrencia como por ejemplo aplicaciones ASP.NET.
En el GC de .NET tiene tres generaciones (0, 1 y 2), y todos los objetos se crean en la generación 0 siempre y cuando no superen el tamaño de 85.000 bytes (enseguida veremos qué pasa con estos objetos). Las colecciones de basura se desencadenan cuando se intenta reservar memoria para un nuevo objeto y se sobrepasa el límite de memoria designado a la generación en cuestión. Los límites de memoria asignados a cada generación se modifican dinámicamente durante la vida del proceso para adaptarse a los patrones de reserva de memoria de la aplicación.
Cuando el GC realiza una colección de basura, revisa todos los objetos de la generación o generaciones afectadas, y comprueba si estan referenciados. Para que un objeto se consideré referenciado, tiene que estar referenciado por un objeto raíz. Los objetos raíz son (simplificando un poco):
· Threads – Todos los objetos referenciados en la pila: variables locales, parámetros, etc.
· Strong Reference – Objetos estáticos, objetos de caché y variables globales.
· Weak Reference – Aunque los objetos WeakReference no evitan que sus objetos referenciados sean “colectados”, se consideran objetos raíz.
· Pinned Objects – Los objetos marcados como Pinned no pueden ser “colectados” ni movidos por el GC, y por tanto los objetos a los que estos referencian tampoco pueden ser colectados. Esta técnica se suele utilizar para pasar un objeto .NET como referencia a una API nativa (no .NET), de forma que la dirección de memoria del objeto .NET no cambie hasta que no finalice la llamada a la API. Los objetos deben permanecer Pinned lo mínimo indispensable dado que pueden causar fragmentación del heap de .NET.
· Objetos que implementan destructor o Finalize() – Esta categoría la trato en un post separado: Cosas que deberías saber sobre los destructores en .NET
Los objetos que no están referenciados serán eliminados por el GC y su espacio en memoria será liberado, y los objetos supervivientes a la colección serán promocionados a la siguiente generación con la excepción de los objetos en la generación 2 que ya no pueden promocionar más. Por último, los "huecos" de espacio libre de los objetos eliminados es consolidado de forma que los objetos supervivientes son reubicados en direcciones de memoria contiguas. Una aplicación con una ratio de colecciones saludable, suele tener 10 veces más colecciones de la generación 0 que de la generación 1, y 10 veces más colecciones de la generación 1 que de la generación 2, es decir un ratio de 100:10:1 para GEN 0:GEN 1:GEN 2.
El colector de basura generacional es indispensable para alcanzar el nivel de rendimiento necesario en una aplicación de alta concurrencia, y se basa en la siguiente regla heurística: los objetos que han existido mucho tiempo, van a seguir existiendo durante mucho tiempo más. Es decir que si un objeto ha sobrevivido a dos colecciones y ha promocionado hasta la generación 2, lo más probable es que vaya a seguir sobreviviendo a colecciones venideras. Por lo tanto no tiene sentido colectar basura con la misma frecuencia en la generación 2 que en la 0. Tras haber realizado miles de pruebas de carga con distintos tipos de aplicaciones, esta presunción ha resultado ser cierta (casi siempre).
Cómo hacía referencia antes, los objetos cuyo tamaño es superior a los 85.000 bytes reciben un trato distinto. Estos objetos se crean en el Large Object Heap (en adelante LOH), también conocido a veces como generación 3. ¿Porqué necesitamos una generación o un heap especial para objetos grandes? Básicamente por dos motivos.
1) Se asume que los objetos grandes generalmente tienen una vida larga (misma regla heurística que para la generación 2).
2) Los objetos grandes son “caros” de mover y por este motivo el espacio libre en el LOH no se consolida y por tanto favorece la fragmentación de memoria. Por esto los objetos grandes se crean en un heap específico.
Cuando se desencadena una colección en la generación 2 o en el LOH, se realiza una colección completa (es decir de las generaciones 0, 1, 2 y LOH). Las colecciones de basura completas son costosas, sobre todo en cuanto a consumo de CPU, dado que potencialmente hay muchos objetos que revisar y mover una vez se ha liberado el espacio.
Dicho esto, ¿qué consideraciones debemos tener en cuanto al uso de memoria cuando desarrollamos aplicaciones .NET? Estas son algunas, pero no dudéis en aportar vuestros propios comentarios:
· Cuidado con los objetos alojados en el LOH. Siempre que tenga sentido, es deseable reutilizarlos y mantenerlos referenciados durante toda la vida del proceso.
· Cuidado con la concatenación de cadenas (String) en un bucle, por ejemplo generando dinámicamente un XML o un fragmento de HTML. Estas prácticas, si no se implementan correctamente, suelen terminar en objetos String de gran tamaño en el LOH que provocan constantes colecciones de basura completas y el correspondiente consumo de 100% CPU. Utilizad la clase StringBuilder para esto.
· Cuidado con los objetos que cacheamos, y las referencias a otros objetos que estos pueden mantener. Cachear objetos indirectamente de forma “involuntaria” es la forma más frecuente de provocar un memory leak en aplicaciones .NET.
Si queréis seguir profundizando en el funcionamiento del GC, os recomiendo los siguientes recursos:
Garbage Collection: Automatic Memory Management in the Microsoft .NET Framework
http://msdn.microsoft.com/en-us/magazine/bb985010.aspx
Garbage Collection Part 2: Automatic Memory Management in the Microsoft .NET Framework
http://msdn.microsoft.com/en-us/magazine/bb985011.aspx
Garbage Collector Basics and Performance Hints
http://msdn.microsoft.com/en-us/library/ms973837.aspx
Maoni's WebLog - CLR Garbage Collector
http://blogs.msdn.com/maoni/
Hasta la próxima,
Si alguna vez os habéis molestado en analizar unos logs de IIS, quizá os hayais encontrado con el escenario en el que cuando tenemos habilitada la autenticación de Windows integrada, cada petición que hacemos realmente necesita tres peticiones para llevarse a cabo. Este es el patrón típico de la autenticación NTLM. La primera petición falla dando un error HTTP 401.2, la segunda falla también pero con un error 401.1, y la tercera es la definitiva devolviendo un resultado HTTP 200 - OK.
Os explico el porqué de esta secuencia:
SECUENCIA
PETICIÓN
RESPUESTA
RESULTADO HTTP
1
El cliente (por ejemplo Internet Explorer) hace una petición GET (o POST) a un servidor IIS. Esta primera petición es anónima, puesto que a priori el cliente no tiene porqué saber nada sobre este servidor: no sabe si el servidor requiere algún tipo de autenticación, no sabe qué tipos de autenticación acepta el servidor, etc.
Supongamos que el servidor sólo tiene habilitada la autenticación de Windows integrada. Puesto que la petición que ha llegado es anónima, el servidor responde con un error HTTP 401.2 (Logon Failed due to server configuration) y le especifica al cliente los métodos de autenticación que acepta. Para este ejemplo supongamos que acepta Negotiate y NTLM.
401.2[Logon Failed due to server configuration]
2
El cliente elige autenticarse mediante NTLM (el motivo de esta elección es irrelevante para el ejemplo) y le pasa un encabezado HTTP al servidor indicando que se va a autenticar por NTLM. En este encabezado indica también la cuenta de usuario con la que se va a autenticar (pero no la contraseña).
Para que el servidor compruebe que el cliente es quién dice ser, necesita comprobar que conoce la contraseña sin pedírsela. Para ello genera un número aleatorio de 16-byte, conocido como el “challenge” (o reto) y se lo envía al cliente. En esta segunda petición, el servidor vuelve a responder con un error HTTP 401.1 (Logon Failed) puesto que el cliente todavía no está autenticado.
401.1[Logon Failed]
3
El cliente cifra el “challenge” con un hash de la contraseña y genera lo que llamamos el “response”. De esta manera, demuestra que conoce dicha contraseña sin que la contraseña en ningún momento se intercambie por la red. Vuelve a enviar la misma petición GET por tercera (y última) vez y con el “response”.
El servidor (que tampoco conoce la contraseña del cliente) envía los siguientes datos al controlador de dominio:
· Nombre de usuario (del cliente)
· El “challenge” enviado al cliente
· El “response” generado por el cliente
El controlador de dominio que sí conoce la contraseña del cliente, cifra el “challenge” con un hash de la contraseña del usuario igual que hizo IE y compara el resultado con el “response” que ha generado IE. Si todo es correcto, por fin se autentica al usuario y se le envía la respuesta definitiva (HTTP 200 –OK).
200[OK]
Una vez hemos visto el motivo de este intercambio de peticiones y respuestas HTTP, más de uno se pregunta ¿esto no genera un excesivo tráfico de red? Las respuesta es que depende. Evidentemente, el protocolo de autenticación NTLM genera más tráfico de red que un sitio web configurado con autenticación anónima, pero el tráfico adicional normalmente no supone un problema.
Por un lado, todos los clientes HTTP que soportan autenticación NTLM implementan una optimización, que implica que en la segunda petición del handshake NTLM únicamente se incluyen los encabezados HTTP (y por lo tanto se excluye el HTTP entity-body, es decir los datos asociados a la petición en caso de que los haya). Os pongo un ejemplo, si la petición en cuestión es un POST de un fichero de 100MB, el primer POST anónimo (junto con los 100MB de fichero adjunto) va a fallar con un error 401.1. Cuando el cliente decide autenticarse por NTLM, hace un segundo POST pero esta vez sin entity-body (es decir, sin los 100MB de fichero adjunto) dado que ya sabe que esta petición también va a fallar inevitablemente con un error 401.1. En la última petición, de nuevo volvemos a incluir el entity-body, que será la petición definitiva y cuando finalmente logremos subir el fichero al servidor HTTP.
Adicionalmente, Internet Explorer (en adelante IE) incluye una característica conocida como pre-autenticación NTLM que evita realizar la primera petición anónima una vez determina que un servidor en concreto acepta autenticación NTLM. La pre-autenticación NTLM directamente envía el encabezado HTTP de autenticación por NTLM (paso 2 de la secuencia) indicando al servidor el nombre de usuario para que este genere el “challenge”.
Esta optimización puede dar problemas si una vez iniciado este comportamiento de IE, vamos a parar a un directorio virtual en el mismo sitio web que no admite autenticación NTLM (sino que únicamente utiliza autenticación anónima). En este caso, IE va a hacer una petición enviando los encabezados HTTP (especificando autenticación por NTLM) esperando un 401.1 por parte del servidor. Puesto que IE “cree” que la petición definitiva va a ser la siguiente, elige no mandar los datos del entity-body para ahorrar ancho de banda. Puesto que el servidor no acepta autenticación NTLM pero si acepta autenticación anónima, ignora el encabezado HTTP de autenticación y responde con un HTTP 200 definitivo. Esto provoca que, por ejemplo, los datos del formulario HTML (o el fichero de 100MB del ejemplo anterior) nunca se lleguen a enviar con el POST. El siguiente artículo habla precisamente de este problema:
You cannot post data to a non-NTLM-authenticated Web site
http://support.microsoft.com/kb/251404/en-us
Si tenemos una aplicación ASP.NET que realiza llamadas a un web service, se autentica por NTLM y queremos evitar la primera petición anónima, podemos implementar la pre-autenticación NTLM de la siguiente manera:
MyWebService proxy = new MyWebService();
proxy.Credentials = CredentialCache.DefaultNetworkCredentials;
proxy.PreAuthenticate = true;
proxy.HelloWorld();
Por último, si queremos reducir aún más el ancho de banda causado por la autenticación NTLM, podemos reducir los Custom Errrors que devuelve IIS para los errores HTTP 401.1 y 401.2.
Happy hacking.
Cuando Microsoft sacó Windows Server 2003 con IIS 6.0 se dio un gran paso adelante en cuanto al paradigma secure by default. A diferencia de anteriores versiones, IIS 6.0 se instalaba con muchas de sus funcionalidades deshabilitadas por defecto (p. ej. extensiones ISAPI y componentes CGI), de forma que cada administrador debía habilitar explícitamente las funcionalidades que realmente fuera a utilizar. En una instalación por defecto de IIS 6.0 sólo se sirve contenido estático.
Con Windows Server 2008 e IIS 7.0 se ha dado un paso más en esta dirección, pero este tema ya lo trataré en un post separado.
En todo caso, aunque la instalación por defecto de IIS 6.0 ya nos proporciona un buen punto de partida en cuanto a seguridad, podemos tomar una serie de precauciones para hacer nuestros servidor menos susceptibles a ser atacados, y en el peor de los casos, mitigar las consecuencias de un eventual ataque exitoso. Estas son algunas de las recomendaciones que damos a nuestros clientes (no están ordenadas por importancia):
Reducir la superficie de ataque del servidor usando la herramienta Security Configuration Wizard for Windows Server 2003.Utilizar esta herramienta es una buena práctica para establecer un buen punto de partida del bastionado de los servidores IIS. Esta herramienta nos ayuda a determinar la funcionalidad mínima requerida para el rol o roles de nuestro servidor y nos permite:
· Deshabilitar servicios no requeridos.
· Bloquear puertos que nos están en uso.
· Aplicar restricciones a los puertos que dejamos abiertos.
· Deshabilitar las Web Extensions de IIS que no vayamos a necesitar.
· Establecer políticas de auditoría eficaces.
Utilizar Host Headers para los sitios web en IIS.El uso de Host Headers previene que un sitio web responda a cualquier otra URL que las que están configuradas como Host Headers. De esta forma podemos evitar ataques de escaneo de IPs. La mayoría de los gusanos se extienden mediante escaneo de IPs, por lo que podemos minimizar las susceptibilidad de nuestro servidor a estos ataques simplemente utilizando host headers.
Almacenar el contenido del sitio web en una partición o unidad de disco distinta a la del sistema y auditar los permisos NTFS. Esto nos permitirá protegernos de ataques del tipo directory traversal o backtracking en los que el hacker intenta acceder a ficheros de sistema mediante rutas relativas para obtener el control de la máquina. Restringir al máximo los permisos NTFS del contenido del sitio web es igualmente muy importante.
Desplegar los servidores IIS publicados en Internet fuera del dominio corporativo.Debido a la exposición de los frontales IIS a los ataques de Internet, siempre es recomendable tenerlos los más separados posible de la intranet corporativa y el dominio de directorio activo. De tal forma mitigaremos el potencial daño al resto de máquinas del dominio si la seguridad del servidor IIS se ve comprometida.
Usar URLScan 3.1 para bloquear peticiones potencialmente peligrosas.
Con URLScan podremos crear reglas para bloquear verbos HTTP específicos, extensiones de ficheros, secuencias de caracteres y aplicarlas de forma individual a uno o varios de los encabezados HTTP de cada petición.
Por ejemplo, podemos crear reglas para mitigar ataques de inyección de SQL, de forma que las peticiones cuyo querystring (o cualquier otro encabezado HTTP relvante como las cookies) cumpla una serie de requisitos serán bloqueadas y nunca llegaran a procesarse por nuestro servidor web.
La herramienta la podemos descargar desde aquí:UrlScan version 3.1 RTW - x86
UrlScan version 3.1 RTW - x64
Algunos recursos sobre la herramienta:
Common URLScan Scenarios
http://learn.iis.net/page.aspx/476/common-urlscan-scenarios/
UrlScan 3.1
http://blogs.iis.net/wadeh/archive/2008/10/31/urlscan-3-1.aspx
Habilitar extended logging de IIS
Los logs de IIS nos pueden proporcionar información valiosa sobre intentos de ataque y ataques exitosos a nuestros servidores web. Es recomendable habilitar extended logging de IIS configurado de modo que se registren todos los campos disponibles (por defecto no se registran todos). Adicionalmente, debemos almacenar los logs en una partición o unidad de disco distinta a la del contenido del sitio web y distinta a la del sistema operativo. Por último debemos restringir los permisos NTFS a la carpeta de logs. De esta forma dificultaremos la tarea del hacker a la hora de borrar sus huellas tras una ataque exitoso. Únicamente tendrán permisos full control sobre esta carpeta los administradores (y el sistema). Posteriormente debemos analizar regularmente dichos logs, lo cual podemos hacer, por ejemplo, utilizando Log Parser (ver post Análisis de logs de IIS utilizando Log Parser).
Auditar regularmente la seguridad de los servidores mediante el Microsoft Baseline Security Analyzer
Esta herramienta nos ayuda a identificar vulnerabilidades causadas por configuraciones y políticas inadecuadas, falta de actualizaciones de seguridad, y adicionalmente realiza una serie de comprobaciones específicas sobre la configuración de IIS.
Microsoft Baseline Security Analyzerhttp://www.microsoft.com/technet/security/tools/mbsahome.mspx
Existen diversos escenarios en los que nos es útil saber qué método de autenticación está utilizando nuestra aplicación web y con qué credenciales se está ejecutando nuestro código. Para poder determinarlo de forma rápida he desarrollado una página ASP.NET que hace estas comprobaciones y muestra el resultado en pantalla.
Este es el código de la página ASPX:
<%@ Page Language="C#" Debug="true" %>
<%@ Import Namespace="System.Threading" %>
<%@ Import Namespace="System.Security.Principal" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
public string AuthType, AuthPackage, WindowsID, HttpContextID, ThreadID;
private AuthTypeEnum _authType;
internal enum AuthTypeEnum
Anonymous,
Negotiate,
NTLM,
Other
_authType = AuthTypeEnum.Other;
GetIdentities();
Response.Headers.Add("Connection", "Close");
private void GetIdentities()
AuthType = GetAuthType();
AuthPackage = GetAuthPackage();
WindowsID = GetWindowsID();
HttpContextID = GetHttpContextID();
ThreadID = GetThreadID();
private string GetAuthType()
if (Context.User.Identity.AuthenticationType != String.Empty)
_authType = AuthTypeEnum.Negotiate;
return Context.User.Identity.AuthenticationType;
else if (!Context.User.Identity.IsAuthenticated)
_authType = AuthTypeEnum.Anonymous;
return "Not Authenticated (Anonymous)";
return "-";
private string GetAuthPackage()
if (_authType != AuthTypeEnum.Anonymous &&
Context.Request.ServerVariables["HTTP_AUTHORIZATION"] != null)
string authHeader =
Context.Request.ServerVariables["HTTP_AUTHORIZATION"];
if (authHeader.StartsWith("Negotiate TlRMTVNTUA"))
return "Kerberos";
return "NTLM";
private string GetWindowsID()
if (WindowsIdentity.GetCurrent().Name != String.Empty)
return WindowsIdentity.GetCurrent().Name;
private string GetHttpContextID()
if (HttpContext.Current.User.Identity.Name != String.Empty)
return HttpContext.Current.User.Identity.Name;
private string GetThreadID()
if (Thread.CurrentPrincipal.Identity.Name != String.Empty)
return Thread.CurrentPrincipal.Identity.Name;
</script>
<head id="Head1" runat="server">
<title>ASP.NET Identity Test</title>
<style type="text/css">
.style_div
font-family: "Consolas";
font-size: 22px;
.left
font-weight: bold;
width: 300px;
.right
color: #FF0000;
</style>
</head>
<body>
<form id="form1" runat="server">
<div class="style_div">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td class="left">
Authentication Type:
</td>
<td class="right">
<% Response.Write(AuthType); %>
</tr>
Authentication Package:
<% Response.Write(AuthPackage); %>
Windows Identity:
<% Response.Write(WindowsID); %>
HttpContext Identity:
<% Response.Write(HttpContextID); %>
Thread Identity:
<% Response.Write(ThreadID); %>
</table>
</div>
</form>
</body>