Obtention de données RAW Soap à partir d'un client de référence Web s'exécutant dans ASP.net

95

J'essaye de dépanner un client de service Web dans mon projet actuel. Je ne suis pas sûr de la plate-forme du serveur de service (très probablement LAMP). Je pense qu'il y a un défaut de leur côté de la clôture car j'ai éliminé les problèmes potentiels avec mon client. Le client est un proxy de référence Web de type ASMX standard généré automatiquement à partir du service WSDL.

Ce dont je dois accéder, ce sont les messages SOAP RAW (demande et réponses)

Quelle est la meilleure manière de s'occuper de ça?

Andrew Harry
la source

Réponses:

135

J'ai apporté les modifications suivantes web.configpour obtenir l'enveloppe SOAP (demande / réponse). Cela affichera toutes les informations SOAP brutes dans le fichier 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
la source
2
Cela ne fonctionne pas pour moi en 02/2012 en utilisant VS 2010. Quelqu'un connaît-il une solution plus à jour?
qxotk
2
Ceci est utile. Cependant, dans mon cas, les réponses sont gzippées et extraire les codes ASCII ou hexadécimaux de la sortie combinée est une douleur. Leurs méthodes de sortie alternatives sont-elles uniquement binaires ou textuelles?
2
@Dediqated - vous pouvez définir le chemin d'accès au fichier trace.log en ajustant la valeur de l'attribut initializeData dans l'exemple ci-dessus.
DougCouto
3
Cela ne sert plus à rien, j'ai utilisé WireShark pour voir les données SOAP brutes. Mais merci quand même!
Dédié
7
Bien. Mais le XML pour moi est comme: 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 ....... Toute idée de comment le formater ou n'avoir que des données xml .?
Habeeb
35

Vous pouvez implémenter une SoapExtension qui enregistre la requête et la réponse complètes dans un fichier journal. Vous pouvez ensuite activer SoapExtension dans le web.config, ce qui facilite l'activation / la désactivation à des fins de débogage. Voici un exemple que j'ai trouvé et modifié pour mon propre usage, dans mon cas, la journalisation a été effectuée par log4net mais vous pouvez remplacer les méthodes de journalisation par les vôtres.

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); }
    }
}

Vous ajoutez ensuite la section suivante à votre web.config où YourNamespace et YourAssembly pointent vers la classe et l'assembly de votre SoapExtension:

<webServices>
  <soapExtensionTypes>
    <add type="YourNamespace.SoapLoggerExtension, YourAssembly" 
       priority="1" group="0" />
  </soapExtensionTypes>
</webServices>
John Lemp
la source
Je vais essayer. J'avais regardé dans les extensions de savon, mais il me semblait que c'était plus pour héberger le service. Je vais vous donner la coche si cela fonctionne :)
Andrew Harry
Oui, cela fonctionnera pour enregistrer les appels effectués à partir de votre application Web via le proxy généré.
John Lemp
C'est l'itinéraire que j'ai suivi, cela fonctionne très bien et je peux utiliser xpath pour masquer toutes les données sensibles avant de les enregistrer.
Dave Baghdanov le
J'avais besoin d'ajouter des en-têtes à la demande de savon sur le client. Cela m'a aidé. rhyous.com/2015/04/29/…
Rhyous
Je suis également nouveau sur c # et je ne sais pas quoi mettre pour le nom de l'assembly. L'extension ne semble pas s'exécuter.
Collin Anderson
28

Je ne sais pas pourquoi tout le problème avec web.config ou une classe de sérialisation. Le code ci-dessous a fonctionné pour moi:

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

using (StringWriter textWriter = new StringWriter())
{
    xmlSerializer.Serialize(textWriter, myEnvelope);
    return textWriter.ToString();
}
Bimmerbound
la source
17
D'où myEnvelopevient-il?
ProfK
6
Je ne comprends pas pourquoi cette réponse n'a pas plus de votes positifs, sans détour! Bien joué!
Batista
1
myEnvalope serait l'objet que vous envoyez via le service Web. Je ne pouvais pas tout à fait faire fonctionner cela comme décrit (en particulier la fonction textWriter.tostring) mais cela fonctionnait assez bien pour mes besoins en mode débogage en ce que vous pouvez voir le XML brut après avoir appelé serialize. J'apprécie beaucoup cette réponse, car certains des autres ont travaillé avec des résultats mitigés (la journalisation à l'aide de web.config n'a renvoyé qu'une partie du xml par exemple, et n'était pas très facile à analyser non plus).
user2366842
@Bimmerbound: cela fonctionnerait-il avec des messages savon sécurisés envoyés via un VPN crypté?
Our Man in Bananas
6
Cette réponse ne produit pas d'enveloppe de savon, elle est simplement sérialisée en xml. Comment obtenir une demande d'enveloppe de savon?
Ali Karaca
21

Essayez Fiddler2, il vous permettra d'inspecter les demandes et la réponse. Il peut être intéressant de noter que Fiddler fonctionne à la fois avec le trafic http et https.

Aaron Fischer
la source
C'est un service chiffré https
Andrew Harry
8
Fiddler peut déchiffrer le trafic https
Aaron Fischer
1
J'ai trouvé cette solution meilleure que l'ajout d'un traçage au web / app.config. Merci!
andyuk
1
Mais l'utilisation de system.diagnostics dans le fichier de configuration ne nécessite pas l'installation d'une application tierce
Azat
1
@AaronFischer: Fiddler fonctionnerait-il avec des messages soap envoyés via un VPN crypté?
Our Man in Bananas
6

Il semble que la solution de Tim Carter ne fonctionne pas si l'appel à la référence Web lève une exception. J'ai essayé d'obtenir la réponse Web brute afin de pouvoir l'examiner (dans le code) dans le gestionnaire d'erreurs une fois l'exception levée. Cependant, je trouve que le journal des réponses écrit par la méthode de Tim est vide lorsque l'appel lève une exception. Je ne comprends pas complètement le code, mais il semble que la méthode de Tim entre dans le processus après le point où .Net a déjà invalidé et ignoré la réponse Web.

Je travaille avec un client qui développe manuellement un service Web avec un codage de bas niveau. À ce stade, ils ajoutent leurs propres messages d'erreur de processus internes sous forme de messages au format HTML dans la réponse AVANT la réponse au format SOAP. Bien sûr, la référence Web automagic .Net explose à ce sujet. Si je pouvais obtenir la réponse HTTP brute après qu'une exception a été lancée, je pourrais rechercher et analyser toute réponse SOAP dans la réponse HTTP de retour mixte et savoir qu'ils ont reçu mes données OK ou non.

Plus tard ...

Voici une solution qui fonctionne, même après une exécution (notez que je ne suis qu'après la réponse - je pourrais aussi obtenir la demande):

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;
        }
    }
}

Voici comment vous le configurez dans le fichier de configuration:

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

"TestCallWebService" doit être remplacé par le nom de la bibliothèque (qui s'est avéré être le nom de l'application de la console de test dans laquelle je travaillais).

Vous ne devriez vraiment pas avoir à accéder à ChainStream; vous devriez pouvoir le faire plus simplement à partir de ProcessMessage comme:

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 vous recherchez SoapMessage.Stream, il est censé être un flux en lecture seule que vous pouvez utiliser pour inspecter les données à ce stade. C'est une mauvaise cause si vous lisez le flux, les bombes de traitement ultérieures sans données ont trouvé des erreurs (le flux était à la fin) et vous ne pouvez pas réinitialiser la position au début.

Fait intéressant, si vous utilisez les deux méthodes, ChainStream et ProcessMessage, la méthode ProcessMessage fonctionnera car vous avez changé le type de flux de ConnectStream en MemoryStream dans ChainStream, et MemoryStream autorise les opérations de recherche. (J'ai essayé de lancer ConnectStream sur MemoryStream - ce n'était pas autorisé.)

Donc ..... Microsoft devrait soit autoriser les opérations de recherche sur le type ChainStream, soit faire de SoapMessage.Stream une véritable copie en lecture seule comme il est censé l'être. (Écrivez à votre membre du Congrès, etc ...)

Un autre point. Après avoir créé un moyen de récupérer la réponse HTTP brute après une exception, je n'ai toujours pas obtenu la réponse complète (comme déterminé par un sniffer HTTP). En effet, lorsque le service Web de développement a ajouté les messages d'erreur HTML au début de la réponse, il n'a pas ajusté l'en-tête Content-Length, de sorte que la valeur Content-Length était inférieure à la taille du corps de la réponse réelle. Tout ce que j'ai obtenu était le nombre de caractères de la valeur Content-Length - le reste manquait. De toute évidence, lorsque .Net lit le flux de réponse, il lit simplement le nombre de caractères Content-Length et ne permet pas que la valeur Content-Length soit éventuellement fausse. C'est comme ça qu'il devrait être; mais si la valeur de l'en-tête Content-Length est fausse, la seule façon d'obtenir le corps de la réponse dans son intégralité est d'utiliser un sniffer HTTPhttp://www.ieinspector.com ).

Chuck Bevitt
la source
2

Je préférerais que le cadre fasse la journalisation pour vous en accrochant un flux de journalisation qui se connecte comme le cadre traite ce flux sous-jacent. Ce qui suit n'est pas aussi propre que je le voudrais, car vous ne pouvez pas choisir entre la demande et la réponse dans la méthode ChainStream. Voici comment je le gère. Remerciements à Jon Hanna pour l'idée dominante d'un stream

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);
        }
    }
}

la source
1
@TimCarter cela a fonctionné pour moi, la seule chose que j'ai supprimée est la partie client, qui me donne un nom avec un deux-points, provoquant une exception. Donc, une fois que j'ai supprimé cette ligne, cela fonctionne très bien pour ceux qui veulent avoir un fichier pour chaque demande / réponse. La seule chose à ajouter est quelque chose d'évident si vous lisez les autres réponses, et c'est que vous devez ajouter le nœud web.config pour indiquer la soapExtension. THX!
netadictos
1

Voici une version simplifiée de la réponse principale. Ajoutez ceci à l' <configuration>élément de votre fichier web.configou App.config. Il créera un trace.logfichier dans le dossier de votre projet bin/Debug. Ou, vous pouvez spécifier un chemin absolu pour le fichier journal à l'aide de l' initializeDataattribut.

  <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>

Il avertit que les attributs maxdatasizeet tracemodene sont pas autorisés, mais ils augmentent la quantité de données qui peuvent être enregistrées et évitent de tout enregistrer en hexadécimal.

Collin Anderson
la source
0

Vous n'avez pas spécifié la langue que vous utilisez, mais en supposant C # / .NET, vous pouvez utiliser des extensions SOAP .

Sinon, utilisez un renifleur tel que Wireshark

rbrayb
la source
1
Oui, j'ai essayé WireShark, il ne peut me montrer que les informations d'en-tête, le contenu du savon est crypté.
Andrew Harry
C'est peut-être parce que vous utilisez https. Pour autant que je me souvienne, les extensions SOAP fonctionnent à un niveau supérieur, vous devriez donc pouvoir voir les données après leur décryptage.
rbrayb
2
Utilisez Fiddler au lieu de Wireshark, il déchiffre https hors de la boîte.
Rodrigo Strauss
-1

Je me rends compte que je suis assez en retard à la fête, et comme la langue n'a pas été spécifiée, voici une solution VB.NET basée sur la réponse de Bimmerbound, au cas où quelqu'un trébucherait sur cela et aurait besoin d'une solution. Remarque: vous devez avoir une référence à la classe stringbuilder dans votre projet, si vous ne l'avez pas déjà.

 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

Appelez simplement la fonction et elle renverra une chaîne avec le xml sérialisé de l'objet que vous essayez de transmettre au service Web (en réalité, cela devrait fonctionner pour tout objet que vous souhaitez lui lancer).

En remarque, l'appel de remplacement dans la fonction avant de renvoyer le xml consiste à supprimer les caractères vbCrLf de la sortie. Le mien en avait un certain nombre dans le xml généré, mais cela variera évidemment en fonction de ce que vous essayez de sérialiser, et je pense qu'ils pourraient être supprimés lors de l'envoi de l'objet au service Web.

user2366842
la source
À quiconque m'a giflé avec le vote défavorable, n'hésitez pas à me faire savoir ce qui me manque / ce qui peut être amélioré.
user2366842