Obtener datos RAW Soap de un cliente de referencia web que se ejecuta en ASP.net

95

Estoy tratando de solucionar problemas con un cliente de servicio web en mi proyecto actual. No estoy seguro de la plataforma del servidor de servicios (probablemente LAMP). Creo que hay una falla en su lado de la cerca, ya que eliminé los problemas potenciales con mi cliente. El cliente es un proxy de referencia web de tipo ASMX estándar generado automáticamente a partir del servicio WSDL.

Lo que necesito para llegar son los mensajes RAW SOAP (solicitud y respuestas)

¿Cuál es la mejor forma de hacerlo?

Andrew Harry
fuente

Respuestas:

135

Hice los siguientes cambios web.configpara obtener el sobre SOAP (solicitud / respuesta). Esto generará toda la información SOAP sin procesar en el archivo trace.log.

<system.diagnostics>
  <trace autoflush="true"/>
  <sources>
    <source name="System.Net" maxdatasize="1024">
      <listeners>
        <add name="TraceFile"/>
      </listeners>
    </source>
    <source name="System.Net.Sockets" maxdatasize="1024">
      <listeners>
        <add name="TraceFile"/>
      </listeners>
    </source>
  </sources>
  <sharedListeners>
    <add name="TraceFile" type="System.Diagnostics.TextWriterTraceListener"
      initializeData="trace.log"/>
  </sharedListeners>
  <switches>
    <add name="System.Net" value="Verbose"/>
    <add name="System.Net.Sockets" value="Verbose"/>
  </switches>
</system.diagnostics>
Keltex
fuente
2
Esto no me funciona en 02/2012 con VS 2010. ¿Alguien conoce una solución más actualizada?
qxotk
2
Esto es útil. Sin embargo, en mi caso, las respuestas están comprimidas en Gzip y extraer los códigos ASCII o hexadecimales de la salida combinada es una molestia. ¿Sus métodos de salida alternativos son binarios o solo de texto?
2
@Dediqated: puede establecer la ruta al archivo trace.log ajustando el valor del atributo initializeData en el ejemplo anterior.
DougCouto
3
Ya no sirve de nada, utilicé wirehark para ver los datos SOAP sin procesar. ¡Pero gracias de todos modos!
Dedicado
7
Bueno. Pero el XML para mí es como: System.Net Verbose: 0: [14088] 00000000: 3C 3F 78 6D 6C 20 76 65-72 73 69 6F 6E 3D 22 31: <? Xml version = "1 System.Net Verbose: 0: [14088] 00000010: 2E 30 22 20 65 6E 63 6F-64 69 6E 67 3D 22 75 74: .0 "encoding =" ut ....... Alguna idea de cómo formatearlo o tener solo datos xml .?
Habeeb
35

Puede implementar una SoapExtension que registre la solicitud y la respuesta completas en un archivo de registro. A continuación, puede habilitar SoapExtension en web.config, lo que facilita su activación / desactivación para fines de depuración. Aquí hay un ejemplo que encontré y modifiqué para mi propio uso, en mi caso, el registro fue realizado por log4net, pero puede reemplazar los métodos de registro con los suyos.

public class SoapLoggerExtension : SoapExtension
{
    private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
    private Stream oldStream;
    private Stream newStream;

    public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
    {
        return null;
    }

    public override object GetInitializer(Type serviceType)
    {
        return null;
    }

    public override void Initialize(object initializer)
    {

    }

    public override System.IO.Stream ChainStream(System.IO.Stream stream)
    {
        oldStream = stream;
        newStream = new MemoryStream();
        return newStream;
    }

    public override void ProcessMessage(SoapMessage message)
    {

        switch (message.Stage)
        {
            case SoapMessageStage.BeforeSerialize:
                break;
            case SoapMessageStage.AfterSerialize:
                Log(message, "AfterSerialize");
                    CopyStream(newStream, oldStream);
                    newStream.Position = 0;
                break;
                case SoapMessageStage.BeforeDeserialize:
                    CopyStream(oldStream, newStream);
                    Log(message, "BeforeDeserialize");
                break;
            case SoapMessageStage.AfterDeserialize:
                break;
        }
    }

    public void Log(SoapMessage message, string stage)
    {

        newStream.Position = 0;
        string contents = (message is SoapServerMessage) ? "SoapRequest " : "SoapResponse ";
        contents += stage + ";";

        StreamReader reader = new StreamReader(newStream);

        contents += reader.ReadToEnd();

        newStream.Position = 0;

        log.Debug(contents);
    }

    void ReturnStream()
    {
        CopyAndReverse(newStream, oldStream);
    }

    void ReceiveStream()
    {
        CopyAndReverse(newStream, oldStream);
    }

    public void ReverseIncomingStream()
    {
        ReverseStream(newStream);
    }

    public void ReverseOutgoingStream()
    {
        ReverseStream(newStream);
    }

    public void ReverseStream(Stream stream)
    {
        TextReader tr = new StreamReader(stream);
        string str = tr.ReadToEnd();
        char[] data = str.ToCharArray();
        Array.Reverse(data);
        string strReversed = new string(data);

        TextWriter tw = new StreamWriter(stream);
        stream.Position = 0;
        tw.Write(strReversed);
        tw.Flush();
    }
    void CopyAndReverse(Stream from, Stream to)
    {
        TextReader tr = new StreamReader(from);
        TextWriter tw = new StreamWriter(to);

        string str = tr.ReadToEnd();
        char[] data = str.ToCharArray();
        Array.Reverse(data);
        string strReversed = new string(data);
        tw.Write(strReversed);
        tw.Flush();
    }

    private void CopyStream(Stream fromStream, Stream toStream)
    {
        try
        {
            StreamReader sr = new StreamReader(fromStream);
            StreamWriter sw = new StreamWriter(toStream);
            sw.WriteLine(sr.ReadToEnd());
            sw.Flush();
        }
        catch (Exception ex)
        {
            string message = String.Format("CopyStream failed because: {0}", ex.Message);
            log.Error(message, ex);
        }
    }
}

[AttributeUsage(AttributeTargets.Method)]
public class SoapLoggerExtensionAttribute : SoapExtensionAttribute
{
    private int priority = 1; 

    public override int Priority
    {
        get { return priority; }
        set { priority = value; }
    }

    public override System.Type ExtensionType
    {
        get { return typeof (SoapLoggerExtension); }
    }
}

Luego agrega la siguiente sección a su web.config donde YourNamespace y YourAssembly apuntan a la clase y ensamblaje de su SoapExtension:

<webServices>
  <soapExtensionTypes>
    <add type="YourNamespace.SoapLoggerExtension, YourAssembly" 
       priority="1" group="0" />
  </soapExtensionTypes>
</webServices>
John Lemp
fuente
Voy a darle una oportunidad. Había buscado extensiones de jabón, pero me parecía que era más para albergar el servicio. Te daré la marca si funciona :)
Andrew Harry
Sí, esto funcionará para registrar las llamadas realizadas desde su aplicación web a través del proxy generado.
John Lemp
Esta es la ruta que seguí, está funcionando muy bien y puedo usar xpath para enmascarar cualquier dato sensible antes de registrarlo.
Dave Baghdanov
Necesitaba agregar encabezados a la solicitud de jabón en el cliente. Eso me ayudó. rhyous.com/2015/04/29/…
Rhyous
También soy nuevo en c # y no estoy seguro de qué poner para el nombre del ensamblado. La extensión no parece ejecutarse.
Collin Anderson
28

No estoy seguro de por qué tanto alboroto con web.config o una clase de serializador. El siguiente código funcionó para mí:

XmlSerializer xmlSerializer = new XmlSerializer(myEnvelope.GetType());

using (StringWriter textWriter = new StringWriter())
{
    xmlSerializer.Serialize(textWriter, myEnvelope);
    return textWriter.ToString();
}
Bimmerbound
fuente
17
De donde myEnvelopeviene
ProfK
6
¡No entiendo por qué esta respuesta no tiene más votos a favor, directa y al grano! ¡Bien hecho!
Batista
1
myEnvalope sería el objeto que está enviando a través del servicio web. No pude hacer que esto funcionara como se describe (particularmente la función textWriter.tostring) pero funcionó lo suficientemente bien para mis propósitos en el modo de depuración, ya que puede ver el xml sin procesar después de llamar a serialize. Aprecio mucho esta respuesta, ya que algunos de los otros han trabajado con resultados mixtos (el registro con web.config solo devolvió parte del xml, por ejemplo, y tampoco fue muy fácil de analizar).
user2366842
@Bimmerbound: ¿funcionaría esto con mensajes de jabón seguros enviados a través de una VPN encriptada?
Our Man in Bananas
6
Esta respuesta no produce un sobre de jabón, solo se serializa en xml. ¿Cómo obtener una solicitud de sobre de jabón?
Ali Karaca
21

Pruebe Fiddler2 , le permitirá inspeccionar las solicitudes y la respuesta. Vale la pena señalar que Fiddler funciona con tráfico http y https.

Aaron Fischer
fuente
Es un servicio encriptado https
Andrew Harry
8
Fiddler puede desencriptar el tráfico https
Aaron Fischer
1
Encontré que esta solución es mejor que agregar rastreo a web / app.config. ¡Gracias!
andyuk
1
Pero el uso de system.diagnostics en el archivo de configuración no requiere la instalación de una aplicación de terceros
Azat
1
@AaronFischer: ¿Funcionaría Fiddler con mensajes de jabón enviados a través de una VPN encriptada?
Our Man in Bananas
6

Parece que la solución de Tim Carter no funciona si la llamada a la referencia web arroja una excepción. He estado tratando de acceder a la resonancia web sin procesar para poder examinarla (en código) en el controlador de errores una vez que se lanza la excepción. Sin embargo, encuentro que el registro de respuesta escrito por el método de Tim está en blanco cuando la llamada arroja una excepción. No entiendo completamente el código, pero parece que el método de Tim interviene en el proceso después del punto en el que .Net ya ha invalidado y descartado la respuesta web.

Estoy trabajando con un cliente que está desarrollando un servicio web manualmente con codificación de bajo nivel. En este punto, están agregando sus propios mensajes de error de proceso interno como mensajes con formato HTML en la respuesta ANTES de la respuesta con formato SOAP. Por supuesto, la referencia web de Automagic .Net explota sobre esto. Si pudiera obtener la respuesta HTTP sin procesar después de que se lanza una excepción, podría buscar y analizar cualquier respuesta SOAP dentro de la respuesta HTTP de retorno mixta y saber que recibieron mis datos bien o no.

Luego ...

Aquí hay una solución que funciona, incluso después de una ejecución (tenga en cuenta que solo estoy después de la respuesta, también podría obtener la solicitud):

namespace ChuckBevitt
{
    class GetRawResponseSoapExtension : SoapExtension
    {
        //must override these three methods
        public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
        {
            return null;
        }
        public override object GetInitializer(Type serviceType)
        {
            return null;
        }
        public override void Initialize(object initializer)
        {
        }

        private bool IsResponse = false;

        public override void ProcessMessage(SoapMessage message)
        {
            //Note that ProcessMessage gets called AFTER ChainStream.
            //That's why I'm looking for AfterSerialize, rather than BeforeDeserialize
            if (message.Stage == SoapMessageStage.AfterSerialize)
                IsResponse = true;
            else
                IsResponse = false;
        }

        public override Stream ChainStream(Stream stream)
        {
            if (IsResponse)
            {
                StreamReader sr = new StreamReader(stream);
                string response = sr.ReadToEnd();
                sr.Close();
                sr.Dispose();

                File.WriteAllText(@"C:\test.txt", response);

                byte[] ResponseBytes = Encoding.ASCII.GetBytes(response);
                MemoryStream ms = new MemoryStream(ResponseBytes);
                return ms;

            }
            else
                return stream;
        }
    }
}

Así es como se configura en el archivo de configuración:

<configuration>
     ...
  <system.web>
    <webServices>
      <soapExtensionTypes>
        <add type="ChuckBevitt.GetRawResponseSoapExtension, TestCallWebService"
           priority="1" group="0" />
      </soapExtensionTypes>
    </webServices>
  </system.web>
</configuration>

"TestCallWebService" debe reemplazarse con el nombre de la biblioteca (que resultó ser el nombre de la aplicación de la consola de prueba en la que estaba trabajando).

Realmente no debería tener que ir a ChainStream; debería poder hacerlo de manera más simple desde ProcessMessage como:

public override void ProcessMessage(SoapMessage message)
{
    if (message.Stage == SoapMessageStage.BeforeDeserialize)
    {
        StreamReader sr = new StreamReader(message.Stream);
        File.WriteAllText(@"C:\test.txt", sr.ReadToEnd());
        message.Stream.Position = 0; //Will blow up 'cause type of stream ("ConnectStream") doesn't alow seek so can't reset position
    }
}

Si busca SoapMessage.Stream, se supone que es un flujo de solo lectura que puede usar para inspeccionar los datos en este punto. Esto es un error porque si lee la transmisión, el procesamiento posterior de bombas sin errores de datos encontrados (la transmisión estaba al final) y no puede restablecer la posición al principio.

Curiosamente, si usa ambos métodos, ChainStream y ProcessMessage, el método ProcessMessage funcionará porque cambió el tipo de flujo de ConnectStream a MemoryStream en ChainStream, y MemoryStream sí permite operaciones de búsqueda. (Intenté transmitir ConnectStream a MemoryStream, no estaba permitido).

Entonces ... Microsoft debería permitir las operaciones de búsqueda en el tipo ChainStream o hacer que SoapMessage.Stream sea realmente una copia de solo lectura como se supone que es. (Escriba a su congresista, etc ...)

Un punto más. Después de crear una forma de recuperar la respuesta HTTP sin procesar después de una excepción, todavía no obtuve la respuesta completa (según lo determinado por un rastreador de HTTP). Esto se debió a que cuando el servicio web de desarrollo agregó los mensajes de error HTML al comienzo de la respuesta, no ajustó el encabezado Content-Length, por lo que el valor Content-Length era menor que el tamaño del cuerpo de la respuesta real. Todo lo que obtuve fue el número de caracteres del valor Content-Length; el resto faltaba. Obviamente, cuando .Net lee el flujo de respuesta, solo lee el número de caracteres Content-Length y no permite que el valor Content-Length posiblemente sea incorrecto. Esto es como debería ser; pero si el valor del encabezado Content-Length es incorrecto, la única forma de obtener el cuerpo de respuesta completo es con un rastreador HTTP (yo utilizo HTTP Analyzer dehttp://www.ieinspector.com ).

Chuck Bevitt
fuente
2

Preferiría que el marco haga el registro por usted conectando un flujo de registro que se registre a medida que el marco procesa ese flujo subyacente. Lo siguiente no es tan limpio como me gustaría, ya que no puede decidir entre solicitud y respuesta en el método ChainStream. Lo siguiente es cómo lo manejo. Agradecimiento a Jon Hanna por la idea primordial de una transmisión.

public class LoggerSoapExtension : SoapExtension
{
    private static readonly string LOG_DIRECTORY = ConfigurationManager.AppSettings["LOG_DIRECTORY"];
    private LogStream _logger;

    public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
    {
        return null;
    }
    public override object GetInitializer(Type serviceType)
    {
        return null;
    }
    public override void Initialize(object initializer)
    {
    }
    public override System.IO.Stream ChainStream(System.IO.Stream stream)
    {
        _logger = new LogStream(stream);
        return _logger;
    }
    public override void ProcessMessage(SoapMessage message)
    {
        if (LOG_DIRECTORY != null)
        {
            switch (message.Stage)
            {
                case SoapMessageStage.BeforeSerialize:
                    _logger.Type = "request";
                    break;
                case SoapMessageStage.AfterSerialize:
                    break;
                case SoapMessageStage.BeforeDeserialize:
                    _logger.Type = "response";
                    break;
                case SoapMessageStage.AfterDeserialize:
                    break;
            }
        }
    }
    internal class LogStream : Stream
    {
        private Stream _source;
        private Stream _log;
        private bool _logSetup;
        private string _type;

        public LogStream(Stream source)
        {
            _source = source;
        }
        internal string Type
        {
            set { _type = value; }
        }
        private Stream Logger
        {
            get
            {
                if (!_logSetup)
                {
                    if (LOG_DIRECTORY != null)
                    {
                        try
                        {
                            DateTime now = DateTime.Now;
                            string folder = LOG_DIRECTORY + now.ToString("yyyyMMdd");
                            string subfolder = folder + "\\" + now.ToString("HH");
                            string client = System.Web.HttpContext.Current != null && System.Web.HttpContext.Current.Request != null && System.Web.HttpContext.Current.Request.UserHostAddress != null ? System.Web.HttpContext.Current.Request.UserHostAddress : string.Empty;
                            string ticks = now.ToString("yyyyMMdd'T'HHmmss.fffffff");
                            if (!Directory.Exists(folder))
                                Directory.CreateDirectory(folder);
                            if (!Directory.Exists(subfolder))
                                Directory.CreateDirectory(subfolder);
                            _log = new FileStream(new System.Text.StringBuilder(subfolder).Append('\\').Append(client).Append('_').Append(ticks).Append('_').Append(_type).Append(".xml").ToString(), FileMode.Create);
                        }
                        catch
                        {
                            _log = null;
                        }
                    }
                    _logSetup = true;
                }
                return _log;
            }
        }
        public override bool CanRead
        {
            get
            {
                return _source.CanRead;
            }
        }
        public override bool CanSeek
        {
            get
            {
                return _source.CanSeek;
            }
        }

        public override bool CanWrite
        {
            get
            {
                return _source.CanWrite;
            }
        }

        public override long Length
        {
            get
            {
                return _source.Length;
            }
        }

        public override long Position
        {
            get
            {
                return _source.Position;
            }
            set
            {
                _source.Position = value;
            }
        }

        public override void Flush()
        {
            _source.Flush();
            if (Logger != null)
                Logger.Flush();
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            return _source.Seek(offset, origin);
        }

        public override void SetLength(long value)
        {
            _source.SetLength(value);
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            count = _source.Read(buffer, offset, count);
            if (Logger != null)
                Logger.Write(buffer, offset, count);
            return count;
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            _source.Write(buffer, offset, count);
            if (Logger != null)
                Logger.Write(buffer, offset, count);
        }
        public override int ReadByte()
        {
            int ret = _source.ReadByte();
            if (ret != -1 && Logger != null)
                Logger.WriteByte((byte)ret);
            return ret;
        }
        public override void Close()
        {
            _source.Close();
            if (Logger != null)
                Logger.Close();
            base.Close();
        }
        public override int ReadTimeout
        {
            get { return _source.ReadTimeout; }
            set { _source.ReadTimeout = value; }
        }
        public override int WriteTimeout
        {
            get { return _source.WriteTimeout; }
            set { _source.WriteTimeout = value; }
        }
    }
}
[AttributeUsage(AttributeTargets.Method)]
public class LoggerSoapExtensionAttribute : SoapExtensionAttribute
{
    private int priority = 1;
    public override int Priority
    {
        get
        {
            return priority;
        }
        set
        {
            priority = value;
        }
    }
    public override System.Type ExtensionType
    {
        get
        {
            return typeof(LoggerSoapExtension);
        }
    }
}

fuente
1
@TimCarter me ha funcionado, lo único que he eliminado es la parte del cliente, que me da un nombre con dos puntos, lo que provoca una excepción. Entonces, una vez que elimino esta línea, funciona bien para aquellos que desean tener un archivo para cada solicitud / respuesta. Lo único que debe agregar es algo obvio si lee las otras respuestas, y es que debe agregar el nodo web.config para indicar la extensión soapExtension. ¡Gracias!
netadictos
1

Aquí hay una versión simplificada de la respuesta principal. Agregue esto al <configuration>elemento de su archivo web.configo App.config. Creará un trace.logarchivo en la bin/Debugcarpeta de su proyecto . O puede especificar una ruta absoluta para el archivo de registro utilizando el initializeDataatributo.

  <system.diagnostics>
    <trace autoflush="true"/>
    <sources>
      <source name="System.Net" maxdatasize="9999" tracemode="protocolonly">
        <listeners>
          <add name="TraceFile" type="System.Diagnostics.TextWriterTraceListener" initializeData="trace.log"/>
        </listeners>
      </source>
    </sources>
    <switches>
      <add name="System.Net" value="Verbose"/>
    </switches>
  </system.diagnostics>

Advierte que los atributos maxdatasizey tracemodeno están permitidos, pero aumentan la cantidad de datos que se pueden registrar y evitan registrar todo en hexadecimal.

Collin Anderson
fuente
0

No ha especificado qué lenguaje está usando, pero asumiendo que C # / .NET podría usar extensiones SOAP .

De lo contrario, use un rastreador como Wireshark

rbrayb
fuente
1
Sí, probé con Wireshark, solo puede mostrarme la información del encabezado, el contenido del jabón está encriptado.
Andrew Harry
Quizás porque estás usando https. Las extensiones SOAP, por lo que puedo recordar, funcionan a un nivel superior, por lo que debería poder ver los datos después de que se hayan descifrado.
rbrayb
2
Use Fiddler en lugar de Wireshark, descifra https desde el primer momento.
Rodrigo Strauss
-1

Me doy cuenta de que llego bastante tarde a la fiesta y, dado que el idioma no se especificó en realidad, aquí hay una solución VB.NET basada en la respuesta de Bimmerbound, en caso de que alguien se encuentre con esto y necesite una solución. Nota: debe tener una referencia a la clase stringbuilder en su proyecto, si aún no la tiene.

 Shared Function returnSerializedXML(ByVal obj As Object) As String
    Dim xmlSerializer As New System.Xml.Serialization.XmlSerializer(obj.GetType())
    Dim xmlSb As New StringBuilder
    Using textWriter As New IO.StringWriter(xmlSb)
        xmlSerializer.Serialize(textWriter, obj)
    End Using


    returnSerializedXML = xmlSb.ToString().Replace(vbCrLf, "")

End Function

Simplemente llame a la función y devolverá una cadena con el xml serializado del objeto que está intentando pasar al servicio web (de manera realista, esto debería funcionar para cualquier objeto que quiera lanzarle también).

Como nota al margen, la llamada de reemplazo en la función antes de devolver el xml es eliminar los caracteres vbCrLf de la salida. El mío tenía un montón de ellos dentro del xml generado, sin embargo, esto obviamente variará dependiendo de lo que esté tratando de serializar, y creo que podrían eliminarse durante el envío del objeto al servicio web.

usuario2366842
fuente
A quien sea que me haya abofeteado con el voto negativo, no dude en hacerme saber qué me estoy perdiendo / qué se puede mejorar.
user2366842