blob: ca4cff4ca10fd1f296137b55eb07c58302e6eefb [file] [log] [blame]
#region Apache License
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to you 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.
//
#endregion
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Linq;
using log4net.Core;
using log4net.Util;
namespace log4net.Appender;
/// <summary>
/// Appender that allows clients to connect via Telnet to receive log messages
/// </summary>
/// <remarks>
/// <para>
/// The TelnetAppender accepts socket connections and streams logging messages back to the client.
/// The output is provided in a telnet-friendly way so that a log can be monitored over a TCP/IP socket.
/// This allows simple remote monitoring of application logging.
/// </para>
/// <para>
/// The default <see cref="Port"/> is 23 (the telnet port).
/// </para>
/// </remarks>
/// <author>Keith Long</author>
/// <author>Nicko Cadell</author>
public class TelnetAppender : AppenderSkeleton
{
private SocketHandler? _handler;
private int _listeningPort = 23;
/// <summary>
/// The fully qualified type of the TelnetAppender class.
/// </summary>
/// <remarks>
/// Used by the internal logger to record the Type of the
/// log message.
/// </remarks>
private static readonly Type _declaringType = typeof(TelnetAppender);
/// <summary>
/// Gets or sets the TCP port number on which this <see cref="TelnetAppender"/> will listen for connections.
/// </summary>
/// <value>
/// An integer value in the range <see cref="IPEndPoint.MinPort" /> to <see cref="IPEndPoint.MaxPort" />
/// indicating the TCP port number on which this <see cref="TelnetAppender"/> will listen for connections.
/// </value>
/// <remarks>
/// <para>
/// The default value is 23 (the telnet port).
/// </para>
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">The value specified is less than <see cref="IPEndPoint.MinPort" />
/// or greater than <see cref="IPEndPoint.MaxPort" />.</exception>
public int Port
{
get => _listeningPort;
set
{
if (value is < IPEndPoint.MinPort or > IPEndPoint.MaxPort)
{
throw SystemInfo.CreateArgumentOutOfRangeException(nameof(value), value,
$"The value specified for Port is less than {IPEndPoint.MinPort} or greater than {IPEndPoint.MaxPort}.");
}
_listeningPort = value;
}
}
/// <summary>
/// Overrides the parent method to close the socket handler
/// </summary>
/// <remarks>
/// <para>
/// Closes all the outstanding connections.
/// </para>
/// </remarks>
protected override void OnClose()
{
base.OnClose();
_handler?.Dispose();
_handler = null;
}
/// <summary>
/// This appender requires a <see cref="Layout"/> to be set.
/// </summary>
protected override bool RequiresLayout => true;
/// <summary>
/// Create the socket handler and wait for connections
/// </summary>
public override void ActivateOptions()
{
base.ActivateOptions();
try
{
LogLog.Debug(_declaringType, $"Creating SocketHandler to listen on port [{_listeningPort}]");
_handler = new SocketHandler(_listeningPort);
}
catch (Exception ex)
{
LogLog.Error(_declaringType, "Failed to create SocketHandler", ex);
throw;
}
}
/// <summary>
/// Writes the logging event to each connected client.
/// </summary>
/// <param name="loggingEvent">The event to log.</param>
protected override void Append(LoggingEvent loggingEvent)
{
if (_handler is not null && _handler.HasConnections)
{
_handler.Send(RenderLoggingEvent(loggingEvent));
}
}
/// <summary>
/// Helper class to manage connected clients
/// </summary>
/// <remarks>
/// <para>
/// The SocketHandler class is used to accept connections from clients.
/// It is threaded so that clients can connect/disconnect asynchronously.
/// </para>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly")]
protected class SocketHandler : IDisposable
{
private const int MaxConnections = 20;
private readonly Socket _serverSocket;
private readonly List<SocketClient> _clients = [];
private readonly object _syncRoot = new();
private bool _wasDisposed;
/// <summary>
/// Class that represents a client connected to this handler
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly")]
protected class SocketClient : IDisposable
{
private readonly Socket _socket;
private readonly StreamWriter _writer;
/// <summary>
/// Create this <see cref="SocketClient"/> for the specified <see cref="Socket"/>
/// </summary>
/// <param name="socket">the client's socket</param>
/// <remarks>
/// <para>
/// Opens a stream writer on the socket.
/// </para>
/// </remarks>
public SocketClient(Socket socket)
{
_socket = socket;
try
{
_writer = new(new NetworkStream(socket));
}
catch (Exception e) when (!e.IsFatal())
{
Dispose();
throw;
}
}
/// <summary>
/// Writes a string to the client.
/// </summary>
/// <param name="message">string to send</param>
public void Send(string message)
{
_writer.Write(message);
_writer.Flush();
}
/// <summary>
/// Cleans up the client connection.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly")]
public void Dispose()
{
try
{
_writer.Dispose();
}
catch (Exception e) when (!e.IsFatal())
{
// Ignore
}
try
{
_socket.Shutdown(SocketShutdown.Both);
}
catch (Exception e) when (!e.IsFatal())
{
// Ignore
}
try
{
_socket.Dispose();
}
catch (Exception e) when (!e.IsFatal())
{
// Ignore
}
}
}
/// <summary>
/// Opens a new server port on <paramref ref="port"/>
/// </summary>
/// <param name="port">the local port to listen on for connections</param>
/// <remarks>
/// <para>
/// Creates a socket handler on the specified local server port.
/// </para>
/// </remarks>
public SocketHandler(int port)
{
_serverSocket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_serverSocket.Bind(new IPEndPoint(IPAddress.Any, port));
_serverSocket.Listen(5);
AcceptConnection();
}
private void AcceptConnection() => _serverSocket.BeginAccept(OnConnect, null);
/// <summary>
/// Sends a string message to each of the connected clients.
/// </summary>
/// <param name="message">the text to send</param>
public void Send(string message)
{
List<SocketClient> localClients;
lock (_syncRoot)
{
localClients = _clients.ToList();
}
// Send outside lock.
foreach (SocketClient client in localClients)
{
try
{
client.Send(message);
}
catch (Exception e) when (!e.IsFatal())
{
// The client has closed the connection, remove it from our list
client.Dispose();
RemoveClient(client);
}
}
}
/// <summary>
/// Add a client to the internal clients list
/// </summary>
/// <param name="client">client to add</param>
private void AddClient(SocketClient client)
{
lock (_syncRoot)
{
_clients.Add(client);
}
}
/// <summary>
/// Remove a client from the internal clients list
/// </summary>
/// <param name="client">client to remove</param>
private void RemoveClient(SocketClient client)
{
lock (_syncRoot)
{
_clients.Remove(client);
}
}
/// <summary>
/// Test if this handler has active connections
/// </summary>
public bool HasConnections => _clients.Count > 0;
// clients.Count is an atomic read that can be done outside the lock.
/// <summary>
/// Callback used to accept a connection on the server socket
/// </summary>
/// <param name="asyncResult">The result of the asynchronous operation</param>
/// <remarks>
/// <para>
/// On connection adds to the list of connections
/// if there are too many open connections you will be disconnected
/// </para>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
private void OnConnect(IAsyncResult asyncResult)
{
if (_wasDisposed)
{
return;
}
try
{
// Block until a client connects
Socket socket = _serverSocket.EndAccept(asyncResult);
LogLog.Debug(_declaringType, $"Accepting connection from [{socket.RemoteEndPoint}]");
SocketClient client = new(socket);
// clients.Count is an atomic read that can be done outside the lock.
int currentActiveConnectionsCount = _clients.Count;
if (currentActiveConnectionsCount < MaxConnections)
{
try
{
client.Send($"TelnetAppender v1.0 ({currentActiveConnectionsCount + 1} active connections)\r\n\r\n");
AddClient(client);
}
catch (Exception e) when (!e.IsFatal())
{
client.Dispose();
}
}
else
{
client.Send("Sorry - Too many connections.\r\n");
client.Dispose();
}
}
catch (Exception e) when (!e.IsFatal())
{
// Ignore
}
finally
{
if (!_wasDisposed)
{
AcceptConnection();
}
}
}
/// <summary>
/// Closes all network connections
/// </summary>
public void Dispose()
{
if (_wasDisposed)
{
return;
}
_wasDisposed = true;
lock (_syncRoot)
{
foreach (SocketClient client in _clients)
{
client.Dispose();
}
_clients.Clear();
try
{
_serverSocket.Shutdown(SocketShutdown.Both);
}
catch (Exception e) when (!e.IsFatal())
{
// Ignore
}
try
{
_serverSocket.Dispose();
}
catch (Exception e) when (!e.IsFatal())
{
// Ignore
}
}
}
}
}