Skip to content

Commit

Permalink
fix: xml escape serialized properties (#52)
Browse files Browse the repository at this point in the history
Correctly XML escape all properties serialized by
`Log4jTextFormatter` and `Log4netTextFormatter`.

Closes #51
  • Loading branch information
FantasticFiasco authored Jul 7, 2019
1 parent badce47 commit 83d0dd0
Show file tree
Hide file tree
Showing 7 changed files with 573 additions and 112 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ This project adheres to [Semantic Versioning](http://semver.org/) and is followi
### :syringe: Fixed

- [#42](https://github.com/FantasticFiasco/serilog-sinks-udp/pull/42) Revert to support IPv4 on networks without IPv6 (contribution by [brettdavis-bmw](https://github.com/brettdavis-bmw))
- [#45](https://github.com/FantasticFiasco/serilog-sinks-udp/pull/45) Correctly XML escape exception message serialized by `Log4jTextFormatter`
- [#45](https://github.com/FantasticFiasco/serilog-sinks-udp/issues/45) Correctly XML escape exception message serialized by `Log4jTextFormatter`
- [#51](https://github.com/FantasticFiasco/serilog-sinks-udp/issues/51) Correctly XML escape all properties serialized by `Log4jTextFormatter` and `Log4netTextFormatter`

### :dizzy: Changed

Expand Down
8 changes: 8 additions & 0 deletions src/Serilog.Sinks.Udp/Sinks/Udp/Private/RemoteEndPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ public RemoteEndPoint(string address, int port)

public int Port { get; }

/// <summary>
/// It's a very small performance optimization to parse the IP address and use it instead
/// of having the HTTP client trying to resolve the address and figure out that it isn't a
/// hostname at all but instead an ordinary IP address.
///
/// A small optimization indeed, but one that was requested by one of the consumers of the
/// package.
/// </summary>
public IPEndPoint IPEndPoint { get; }
}
}
115 changes: 115 additions & 0 deletions src/Serilog.Sinks.Udp/Sinks/Udp/Private/XmlSerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright 2015-2019 Serilog Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System.IO;
using System.Text;

namespace Serilog.Sinks.Udp.Private
{
/// <remarks>
/// The methods in this class where influenced by
/// https://weblog.west-wind.com/posts/2018/Nov/30/Returning-an-XML-Encoded-String-in-NET.
/// </remarks>
internal class XmlSerializer
{
private const char LtCharacter = '<';
private const char GtCharacter = '>';
private const char AmpCharacter = '&';
private const char QuotCharacter = '\"';
private const char AposCharacter = '\'';
private const char LfCharacter = '\n';
private const char CrCharacter = '\r';
private const char TabCharacter = '\t';

private const string LfString = "\n";
private const string CrString = "\r";
private const string TabString = "\t";

private const string SerializedLt = "&lt;";
private const string SerializedGt = "&gt;";
private const string SerializedAmp = "&amp;";
private const string SerializedQuot = "&quot;";
private const string SerializedApos = "&apos;";
private const string SerializedLf = "&#xA;";
private const string SerializedCr = "&#xD;";
private const string SerializedTab = "&#x9;";

internal void SerializeXmlValue(TextWriter output, string text, bool isAttribute)
{
foreach (var character in text)
{
output.Write(SerializeXmlValue(character, isAttribute));
}
}

internal string SerializeXmlValue(string text, bool isAttribute)
{
var builder = new StringBuilder();

foreach (var character in text)
{
builder.Append(SerializeXmlValue(character, isAttribute));
}

return builder.ToString();
}

private static string SerializeXmlValue(char character, bool isAttribute)
{
if (character == LtCharacter)
{
return SerializedLt;
}

if (character == GtCharacter)
{
return SerializedGt;
}

if (character == AmpCharacter)
{
return SerializedAmp;
}

// Special handling for quotes
if (isAttribute && character == QuotCharacter)
{
return SerializedQuot;
}

if (isAttribute && character == AposCharacter)
{
return SerializedApos;
}

// Legal sub-chr32 characters
if (character == LfCharacter)
{
return isAttribute ? SerializedLf : LfString;
}

if (character == CrCharacter)
{
return isAttribute ? SerializedCr : CrString;
}

if (character == TabCharacter)
{
return isAttribute ? SerializedTab : TabString;
}

return character.ToString();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Copyright 2015-2019 Serilog Contributors
//
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//
// http://www.apache.org/licenses/LICENSE-2.0
//
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Expand All @@ -16,17 +16,28 @@
using Serilog.Formatting;
using System.IO;
using System.Xml;
using Serilog.Sinks.Udp.Private;

namespace Serilog.Sinks.Udp.TextFormatters
{
/// <summary>
/// Text formatter serializing log events into log4j complient XML.
/// Text formatter serializing log events into log4j compliant XML.
/// </summary>
public class Log4jTextFormatter : ITextFormatter
{
private static readonly string SourceContextPropertyName = "SourceContext";
private static readonly string ThreadIdPropertyName = "ThreadId";

private readonly XmlSerializer xmlSerializer;

/// <summary>
/// Initializes a new instance of the <see cref="Log4jTextFormatter"/> class.
/// </summary>
public Log4jTextFormatter()
{
xmlSerializer = new XmlSerializer();
}

/// <summary>
/// Format the log event into the output.
/// </summary>
Expand All @@ -49,11 +60,12 @@ public void Format(LogEvent logEvent, TextWriter output)
output.Write("</log4j:event>");
}

private static void WriteLogger(LogEvent logEvent, TextWriter output)
private void WriteLogger(LogEvent logEvent, TextWriter output)
{
if (logEvent.Properties.TryGetValue(SourceContextPropertyName, out LogEventPropertyValue sourceContext))
if (logEvent.Properties.TryGetValue(SourceContextPropertyName, out var sourceContext))
{
output.Write($" logger=\"{((ScalarValue)sourceContext).Value}\"");
var sourceContextValue = ((ScalarValue)sourceContext).Value.ToString();
output.Write($" logger=\"{xmlSerializer.SerializeXmlValue(sourceContextValue, true)}\"");
}
}

Expand Down Expand Up @@ -98,62 +110,32 @@ private static void WriteLevel(LogEvent logEvent, TextWriter output)
output.Write($" level=\"{level}\"");
}

private static void WriteThread(LogEvent logEvent, TextWriter output)
private void WriteThread(LogEvent logEvent, TextWriter output)
{
if (logEvent.Properties.TryGetValue(ThreadIdPropertyName, out LogEventPropertyValue threadId))
if (logEvent.Properties.TryGetValue(ThreadIdPropertyName, out var threadId))
{
output.Write($" thread=\"{((ScalarValue)threadId).Value}\"");
var threadIdValue = ((ScalarValue)threadId).Value.ToString();
output.Write($" thread=\"{xmlSerializer.SerializeXmlValue(threadIdValue, true)}\"");
}
}

private static void WriteMessage(LogEvent logEvent, TextWriter output)
private void WriteMessage(LogEvent logEvent, TextWriter output)
{
output.Write("<log4j:message>");
logEvent.RenderMessage(output);
xmlSerializer.SerializeXmlValue(output, logEvent.RenderMessage(), false);
output.Write("</log4j:message>");
}

private static void WriteException(LogEvent logEvent, TextWriter output)
private void WriteException(LogEvent logEvent, TextWriter output)
{
if (logEvent.Exception == null)
{
return;
}

output.Write("<log4j:throwable>");
EscapeXmlPropertyValue(output, logEvent.Exception.ToString());
xmlSerializer.SerializeXmlValue(output, logEvent.Exception.ToString(), false);
output.Write("</log4j:throwable>");
}

/// <remarks>
/// This method has been influenced by
/// https://weblog.west-wind.com/posts/2018/Nov/30/Returning-an-XML-Encoded-String-in-NET
/// and does not support XML attribute values. The method in the article does, but this
/// doesn't.
/// </remarks>
private static void EscapeXmlPropertyValue(TextWriter output, string text)
{
foreach (var character in text)
{
switch (character)
{
case '<':
output.Write("&lt;");
break;

case '>':
output.Write("&gt;");
break;

case '&':
output.Write("&amp;");
break;

default:
output.Write(character);
break;
}
}
}
}
}
Loading

0 comments on commit 83d0dd0

Please sign in to comment.