Easier log file retrieval in EPiServer 7.5 for environments with limited access to servers

I wrote a small EPiServer admin tool the other day in order to make it easier to retrieve log files without having direct access to the webserver. It is a rather straight forward piece that lists the files of a certain directory, and writes their content to the response output stream when clicked on.

Simple EPiServer 7.5 log retrieval tool in admin mode.

Limitations include the tool not taking load balanced multi server environments into account, meaning that you’ll get the log(s) from whatever server you happen to be using for the moment; this is not an issue in the project that I wrote it for as there are not multiple servers yet. However, we will have to sort it further on. You may also want to limit the size of your log files as you might not want to handle hundreds of megabytes of data.

NOTE: Did a few changes since first post to take a security issue into account.

So, first we have the LogFileItem class. It is a simple DTO used to carry data about each individual log file to the view.

LogFileItem.cs

public class LogFileItem
{
  public string Server { get; set; }
  public string Name { get; set; }
  public double Size { get; set; }
  public DateTime Created { get; set; }
  public DateTime Changed { get; set; }
}

Next there is a LogFileService used to sort out the file handling and separate logic. The corresponding interface exposes the two methods LogFiles and ReadFile; the first one returns a list of DTOs and the latter reads a single file. You may want to use wrappers for the static methods should you want to mock them.

LogFileService

public interface ILogFileService
{
  IEnumerable<LogFileItem> LogFiles();
  string ReadFile(LogFileItem item);
  string PhysicalPathTo(LogFileItem item = null);
}

public class LogFileService : ILogFileService
{
  public IEnumerable<LogFileItem> LogFiles()
  {
    var filenames = Directory.GetFiles(PhysicalPathTo());

    foreach (var filename in filenames)
    {
      var fileInfo = new FileInfo(filename);
      yield return new LogFileItem
        {
          Server = Environment.MachineName,
          Name = fileInfo.Name,
          Size = fileInfo.Length,
          Changed = fileInfo.LastWriteTime,
          Created = fileInfo.CreationTime
        };
    }
  }

  public string ReadFile(LogFileItem item)
  {
    var physicalPath = PhysicalPathTo(item);
    if (!File.Exists(physicalPath))
    {
      throw new FileNotFoundException("Unable to locate file.");
    }
    return File.ReadAllText(physicalPath);
  }

  public string PhysicalPathTo(LogFileItem item = null)
  {
    var file = item != null ? item.Name : string.Empty;
    var logDir = ConfigurationManager.AppSettings["LogDirectoryName"] ?? "Logs";
    return Path.Combine(HttpContext.Current.Server.MapPath("~"), logDir, file);
  }
}

The controller class uses EPiServer’s ServiceLocator in order to retrieve an instance of the file service. This is then used in the two methods for rendering the file list, as well as writing file content to the output stream of the response.

LogDisplayPluginController.cs

[EPiServer.PlugIn.GuiPlugIn(
  Area = EPiServer.PlugIn.PlugInArea.AdminMenu,
  Category = "MyCategory",
  Url = "/modules/MyWeb/LogDisplayPlugin/Index",
  DisplayName = "Log Retrieval"
)]
public class LogDisplayPluginController : Controller
{
  private readonly ILogFileService _logFileService;

  public LogDisplayPluginController()
  {
    _logFileService = ServiceLocator.Current.GetInstance<ILogFileService>();
  }

  public ActionResult Index()
  {
    var files = _logFileService
                  .LogFiles()
                  .OrderByDescending(f => f.Changed);
                  return View(files);
  }

  [HttpGet]
  public ActionResult LogFileStream(LogFileItem item)
  {
    if (IsBadItem(item))
    {
      throw new Exception("Something went wrong with your request.");
    }
    return new LogFileResult(item.Name, stream => WriteFrom(item, stream));
  }

  private bool IsBadItem(LogFileItem item)
  {
    return !item.Name.EndsWith(".log") ||
            item.Name.Contains("/") ||
            item.Name.Contains(@"\") ||
            item.Name.Contains(":") ||
           !File.Exists(logFileService.PhysicalPathTo(item));
  }

  private void WriteFrom(LogFileItem item, Stream stream)
  {
    var content = logFileService.ReadFile(item);
    var bytes = Encoding.UTF8.GetBytes(content);
    stream.Write(bytes, 0, bytes.Length);
  }
}

The LogFileResponse returned inherits from the MVC FileResult and is rather straight forward.

public class LogFileResult : FileResult
{
  private readonly Action<Stream> content;

  public LogFileResult(string fileName, Action<Stream> content, string contentType = "text/plain")
    : base(contentType)
  {
    if (content == null) throw new ArgumentNullException("content");

    this.content = content;
    this.FileDownloadName = fileName;
  }

  protected override void WriteFile(HttpResponseBase response)
  {
    this.content(response.OutputStream);
  }
}

There is not much to say about the view either. The model consists of an IEnumerable of DTOs holding the file information, which is used in a foreach loop to output the proper markup.

Index.cshtml

@model IEnumerable<MyWeb.Core.Models.System.LogFileItem>
@{
  Layout = "../Shared/Layouts/_Admin.cshtml";
}
<div class="epi-contentContainer epi-padding">
  <div class="epi-contentArea">
    <h1 class="EP-prefix">Log retrieval tool</h1>
    <p class=EP-systemInfo>
      Tool for easy access to log files on the server
    </p>

    <table class="epi-default" cellspacing="0" style="border-style: None; border-collapse: collapse;">
    <tr>
      <th class="epitableheading" scope="col">Server</th>
      <th class="epitableheading" scope="col">File name</th>
      <th class="epitableheading" scope="col">Size</th>
      <th class="epitableheading" scope="col">Changed</th>
      <th class="epitableheading" scope="col">Created</th>
    </tr>
    @foreach (var item in Model)
    {
      <tr>
        <td>@item.Server</td>
        <td>@Html.ActionLink(item.Name, "LogFileStream", "LogDisplayPluginController", item, null)</td>
        <td>@item.Size byte(s)</td>
        <td>@item.Changed.ToString("yyyy-MM-dd HH:mm:ss")</td>
        <td>@item.Created.ToString("yyyy-MM-dd HH:mm:ss")</td>
      </tr>
    }
    </table>
  </div>
</div>

2 Comments

  1. SC August 15, 2014
    • Mathias Kunto August 23, 2014