在服务器端记录SOAP请求和响应

时间:2014-01-19 15:08:45

标签: c# asp.net web-services soap soap-extension

我正在尝试为我的ASP.NET Web服务的所有SOAP方法调用创建一个日志记录服务。我一直在看 Log SOAP messages from a console application以及MSDN(http://msdn.microsoft.com/en-us/library/s25h0swd%28v=vs.100%29.aspx)上SOAP扩展的演练,但它们似乎并没有完全覆盖它。

我不想更改SOAP消息,只需将其记录到数据库表即可。 我正在尝试做的是读取SOAP消息流,将其解析为XML,记录XML并让调用以其快乐的方式进行。但是当我读取它时,它被花费/处理掉了。我试过复制流内容不会中断流程。

根据演练,ProcessMessage方法看起来像这样:

public override void ProcessMessage(SoapMessage message) 
{
   switch (message.Stage) 
   {
   case SoapMessageStage.BeforeSerialize:
       break;
   case SoapMessageStage.AfterSerialize:
       // Write the SOAP message out to a file.
       WriteOutput( message );
       break;
   case SoapMessageStage.BeforeDeserialize:
       // Write the SOAP message out to a file.
       WriteInput( message );
       break;
   case SoapMessageStage.AfterDeserialize:
       break;
   default:
       throw new Exception("invalid stage");
   }
}

我已设法在BeforeDeserialize阶段解析流而没有问题,但在ProcessMessage阶段再次调用AfterSerialize,然后使用流,不再使用包含任何数据。

根据SOAP消息修改使用SOAP扩展(http://msdn.microsoft.com/en-us/library/esw638yk%28v=vs.100%29.aspx),SOAP调用将执行以下步骤:

  

服务器端接收请求消息并准备响应

     
      
  1. Web服务器上的ASP.NET接收SOAP消息。
  2.   
  3. 在Web服务器上创建SOAP扩展的新实例。
  4.   
  5. 在Web服务器上,如果这是第一次在服务器端使用此Web服务执行此SOAP扩展,则会在服务器上运行的SOAP扩展上调用GetInitializer方法。
  6.   
  7. 调用Initialize方法。
  8.   
  9. 调用ChainStream方法。
  10.   
  11. 调用ProcessMessage方法,并将SoapMessageStage设置为 BeforeDeserialize
  12.   
  13. ASP.NET反序列化XML中的参数。
  14.   
  15. 调用ProcessMessage方法,并将SoapMessageStage设置为 AfterDeserialize
  16.   
  17. ASP.NET创建实现Web服务的类的新实例,并调用Web服务方法,传入反序列化的参数。该对象与Web服务器位于同一台计算机上。
  18.   
  19. Web服务方法执行其代码,最终设置返回值和任何输出参数。
  20.   
  21. 调用ProcessMessage方法,并将SoapMessageStage设置为 BeforeSerialize。
  22.   
  23. Web服务器上的ASP.NET将返回值和输出参数序列化为XML。
  24.   
  25. 在SoapMessageStage设置为 AfterSerialize 的情况下调用ProcessMessage方法。
  26.   
  27. ASP.NET通过网络将SOAP响应消息发送回Web服务客户端。
  28.   

正确执行步骤6并记录SOAP XML。然后,在服务器处理完呼叫之后,不应该做任何事情,完成它所需要的(步骤10),并返回响应(步骤13)。相反,它会立即在ProcessMessage阶段再次调用AfterSerialize,但这次流已经用完,并在我尝试登录时抛出异常。

根据演练,我应该使用ChainStream方法,它应该在上面的步骤5中运行。当我拨打电话时,它会在<{1}}之前运行两次,在BeforeDeserialize之前运行一次。

我尝试将邮件流复制到单独的流中并将其用于日志记录,如果AfterSerialize已经运行,也会设置某种状态,但问题仍然存在。

我仍然需要BeforeDeserialize中的代码来处理发送回客户端的响应。但是,如果我尝试删除AfterSerialize中的代码并仅在AfterSerialize HTTP 400:错误请求中运行代码。

这一切都发生在实际的方法调用之前,所以我从来没有得到方法内的代码(步骤10)。

2 个答案:

答案 0 :(得分:7)

我的解决方案基于mikebridge的解决方案,但我不得不做一些改动。必须包含初始化程序,如果您尝试在不可用的阶段访问soap消息信息,则会抛出异常。

public class SoapLoggingExtension : SoapExtension
{
    private Stream _originalStream;
    private Stream _workingStream;
    private static String _initialMethodName;
    private static string _methodName;
    private static String _xmlResponse;

    /// <summary>
    /// Side effects: saves the incoming stream to
    /// _originalStream, creates a new MemoryStream
    /// in _workingStream and returns it.  
    /// Later, _workingStream will have to be created
    /// </summary>
    /// <param name="stream"></param>
    /// <returns></returns>
    public override Stream ChainStream(Stream stream)
    {

        _originalStream = stream;
        _workingStream = new MemoryStream();
        return _workingStream;
    }

    /// <summary>
    /// Process soap message
    /// </summary>
    /// <param name="message"></param>
    public override void ProcessMessage(SoapMessage message)
    {
        switch (message.Stage)
        {
            case SoapMessageStage.BeforeSerialize:
                break;

            case SoapMessageStage.AfterSerialize:
                //Get soap call as a xml string
                var xmlRequest = GetSoapEnvelope(_workingStream);
                //Save the inbound method name
                _methodName = message.MethodInfo.Name;
                CopyStream(_workingStream, _originalStream);
                //Log call
                LogSoapRequest(xmlRequest, _methodName, LogObject.Direction.OutPut);
                break;

            case SoapMessageStage.BeforeDeserialize:
                CopyStream(_originalStream, _workingStream);
                //Get xml string from stream before it is used
                _xmlResponse = GetSoapEnvelope(_workingStream);
                break;

            case SoapMessageStage.AfterDeserialize:
                //Method name is only available after deserialize
                _methodName = message.MethodInfo.Name;
                LogSoapRequest(_xmlResponse, _methodName, LogObject.Direction.InPut);
                break;
        }
    }

    /// <summary>
    /// Returns the XML representation of the Soap Envelope in the supplied stream.
    /// Resets the position of stream to zero.
    /// </summary>
    private String GetSoapEnvelope(Stream stream)
    {
        stream.Position = 0;
        StreamReader reader = new StreamReader(stream);
        String data = reader.ReadToEnd();
        stream.Position = 0;
        return data;
    }

    private void CopyStream(Stream from, Stream to)
    {
        TextReader reader = new StreamReader(from);
        TextWriter writer = new StreamWriter(to);
        writer.WriteLine(reader.ReadToEnd());
        writer.Flush();
    }

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

    //Never needed to use this initializer, but it has to be implemented
    public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
    {
        throw new NotImplementedException();
        //return ((TraceExtensionAttribute)attribute).Filename;
    }

    public override void Initialize(object initializer)
    {
        if (String.IsNullOrEmpty(_methodName))
        {
            _initialMethodName = _methodName;
            _waitForResponse = false;
        }
    }

    private void LogSoapRequest(String xml, String methodName, LogObject.Direction direction)
    {

        String connectionString = String.Empty;
        String callerIpAddress = String.Empty;
        String ipAddress = String.Empty;

        try
        {
            //Only log outbound for the response to the original call
            if (_waitForResponse && xml.IndexOf("<" + _initialMethodName + "Response") < 0)
            {
                return;
            }

            if (direction == LogObject.Direction.InPut) {
                _waitForResponse = true;
                _initialMethodName = methodName;
            }

            connectionString = GetSqlConnectionString();
            callerIpAddress = GetClientIp();
            ipAddress = GetClientIp(HttpContext.Current.Request.UserHostAddress);

            //Log call here

            if (!String.IsNullOrEmpty(_methodName) && xml.IndexOf("<" + _initialMethodName + "Response") > 0)
            {
                //Reset static values to initial
                _methodName = String.Empty;
                _initialMethodName = String.Empty;
                _waitForResponse = false;
            }
        }
        catch (Exception ex)
        {
            //Error handling here
        }
    }
    private static string GetClientIp(string ip = null)
    {
        if (String.IsNullOrEmpty(ip))
        {
            ip = HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
        }
        if (String.IsNullOrEmpty(ip) || ip.Equals("unknown", StringComparison.OrdinalIgnoreCase))
        {
            ip = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];
        }
        if (ip == "::1")
            ip = "127.0.0.1";
        return ip;
    }
}

methodName变量用于确定我们正在等待响应的入站呼叫。这当然是可选的,但在我的解决方案中,我对其他web服务进行了几次调用,但我想只记录对第一次调用的响应。

第二部分是你需要在web.config中添加正确的行。显然,不包括整个类类型定义是敏感的(在this example中只定义了类名,这不起作用。该类从未被初始化。):

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.web>
    <webServices>
        <soapExtensionTypes>
            <add group="High" priority="1" type="WsNs.SoapLoggingExtension, WsNs, Version=1.0.0.0, Culture=neutral" />
        </soapExtensionTypes>
    </webServices>
</system.web>
</configuration>

答案 1 :(得分:1)

这是我的第一次拍摄,灵感来自thisthis

SoapExtension在处理流时以及变量初始化或未初始化时具有各种副作用和隐藏的时间依赖性,因此这是一个脆弱的代码。我发现关键是将原始流复制到内存流中,然后在恰当的时刻再次返回。

public class SoapLoggingExtension : SoapExtension
{

    private Stream _originalStream;
    private Stream _workingStream;
    private string _methodName;
    private List<KeyValuePair<string, string>> _parameters;
    private XmlDocument _xmlResponse;
    private string _url;

    /// <summary>
    /// Side effects: saves the incoming stream to
    /// _originalStream, creates a new MemoryStream
    /// in _workingStream and returns it.  
    /// Later, _workingStream will have to be created
    /// </summary>
    /// <param name="stream"></param>
    /// <returns></returns>
    public override Stream ChainStream(Stream stream)
    {

        _originalStream = stream;
        _workingStream = new MemoryStream();
        return _workingStream;
    }

    /// <summary>
    /// AUGH, A TEMPLATE METHOD WITH A SWITCH ?!?
    /// Side-effects: everywhere
    /// </summary>
    /// <param name="message"></param>
    public override void ProcessMessage(SoapMessage message)
    {
        switch (message.Stage)
        {
            case SoapMessageStage.BeforeSerialize:
                break;

            case SoapMessageStage.AfterSerialize:

                var xmlRequest = GetSoapEnvelope(_workingStream);
                CopyStream(_workingStream, _originalStream);
                LogResponse(xmlRequest, GetIpAddress(), _methodName, _parameters); // final step
                break;

            case SoapMessageStage.BeforeDeserialize:

                CopyStream(_originalStream, _workingStream);
                _xmlResponse = GetSoapEnvelope(_workingStream);
                _url = message.Url;
                break;

            case SoapMessageStage.AfterDeserialize:

                SaveCallInfo(message);                    
                break;
        }
    }

    private void SaveCallInfo(SoapMessage message)
    {
        _methodName = message.MethodInfo.Name;

        // the parameter value is converted to a string for logging, 
        // but this may not be suitable for all applications.
        ParameterInfo[] parminfo = message.MethodInfo.InParameters;
        _parameters = parminfo.Select((t, i) => new KeyValuePair<string, String>(
                t.Name, Convert.ToString(message.GetInParameterValue(i)))).ToList();

    }

    private void LogResponse(
        XmlDocument xmlResponse,
        String ipaddress,
        string methodName, 
        IEnumerable<KeyValuePair<string, string>> parameters)
    {
        // SEND TO LOGGER HERE!
    }

    /// <summary>
    /// Returns the XML representation of the Soap Envelope in the supplied stream.
    /// Resets the position of stream to zero.
    /// </summary>
    private XmlDocument GetSoapEnvelope(Stream stream)
    {
        XmlDocument xml = new XmlDocument();
        stream.Position = 0;
        StreamReader reader = new StreamReader(stream);
        xml.LoadXml(reader.ReadToEnd());
        stream.Position = 0;
        return xml;
    }

    private void CopyStream(Stream from, Stream to)
    {
        TextReader reader = new StreamReader(from);
        TextWriter writer = new StreamWriter(to);
        writer.WriteLine(reader.ReadToEnd());
        writer.Flush();
    }

    // GLOBAL VARIABLE DEPENDENCIES HERE!!
    private String GetIpAddress()
    {
        try
        {
            return HttpContext.Current.Request.UserHostAddress;
        }
        catch (Exception)
        {
            // ignore error;
            return "";
        }
    }