// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Web;
using System.Text;
using Tools.CrashReporter.CrashReportCommon;
using Tools.DotNETCommon.XmlHandler;
using System.ComponentModel;
using System.Threading;
namespace Tools.CrashReporter.CrashReportReceiver
{
///
/// A class to handle the receiving of crash reports.
///
public class WebHandler : IDisposable
{
/// A flag to check to ensure the service started properly.
public bool bStartedSuccessfully = false;
/// Base http listener to which the async listeners are associated.
private HttpListener ServiceHttpListener = null;
/// Intermediate path crash reports are downloaded to until complete.
private string FileReceiptPath;
/// Object to take care of incoming files.
LandingZoneMonitor LandingZone;
/// Synchronisation object to cope with erratic client behaviour.
FUploadsInProgress UploadsInProgress = new FUploadsInProgress();
/// Leave at least this amount of time to leave between checks for abandoned reports
const int MinimumMinutesBetweenAbandonedReportChecks = 30;
/// Once a report is this old without being completed, it gets deleted
const int AgeMinutesToConsiderReportAbandoned = 4*60;
/// Time of most recent check for abandoned incomplete reports
DateTime LastAbandonedReportCheckTime = DateTime.Now;
/// Last day, used to create a new log file.
int LastDay = DateTime.UtcNow.Day;
/// Atomic flag to indicate a thread is checking for abandoned reports
int CheckingForAbandonedReports = 0;
///
/// Implementing Dispose.
///
public void Dispose()
{
Dispose( true );
GC.SuppressFinalize( this );
}
///
/// Disposes the resources.
///
/// true if the Dispose call is from user code, and not system code.
protected virtual void Dispose( bool Disposing )
{
if (UploadsInProgress != null)
{
UploadsInProgress.Dispose();
UploadsInProgress = null;
}
if (ServiceHttpListener != null)
{
ServiceHttpListener.Close();
}
}
///
/// Initialise the service to listen for reports.
///
public WebHandler()
{
try
{
FileReceiptPath = Properties.Settings.Default.CrashReportRepository + "-Temp";
LandingZone = new LandingZoneMonitor(
Properties.Settings.Default.CrashReportRepository,
CrashReporterReceiverServicer.Log
);
Directory.CreateDirectory( FileReceiptPath );
// Fire up a listener.
ServiceHttpListener = new HttpListener();
ServiceHttpListener.Prefixes.Add( "http://*:57005/CrashReporter/" );
ServiceHttpListener.Start();
ServiceHttpListener.BeginGetContext( AsyncHandleHttpRequest, null );
bStartedSuccessfully = true;
}
catch( Exception Ex )
{
CrashReporterReceiverServicer.WriteEvent( "Initialisation error: " + Ex.ToString() );
}
}
///
/// Cleanup any used resources.
///
public void Release()
{
if( bStartedSuccessfully )
{
if( ServiceHttpListener != null )
{
ServiceHttpListener.Stop();
ServiceHttpListener.Abort();
}
}
}
///
/// Read in the web request payload as a string.
///
/// A listener request.
/// A string of the input stream in the requests encoding.
private string GetContentStreamString( HttpListenerRequest Request )
{
string Result = "";
if( Request.HasEntityBody )
{
using( StreamReader Reader = new StreamReader( Request.InputStream, Request.ContentEncoding ) )
{
Result = Reader.ReadToEnd();
}
Request.InputStream.Close();
}
return Result;
}
///
/// Check to see if a report has already been uploaded.
///
/// A request containing either the Report Id as a string or an XML representation of a CheckReportRequest class instance.
/// Result object, indicating whether the report has already been uploaded.
private CrashReporterResult CheckReport(HttpListenerRequest Request)
{
CrashReporterResult ReportResult = new CrashReporterResult();
#if DISABLED_CRR
ReportResult.bSuccess = false;
CrashReporterReceiverServicer.WriteEvent("CheckReport() Report rejected by disabled CRR");
#else
var RequestClass = new CheckReportRequest();
RequestClass.ReportId = GetReportIdFromPostData(GetContentStreamString(Request));
ReportResult.bSuccess = !LandingZone.HasReportAlreadyBeenReceived(RequestClass.ReportId);
if( !ReportResult.bSuccess )
{
CrashReporterReceiverServicer.WriteEvent( string.Format( "Report \"{0}\" has already been received", RequestClass.ReportId ) );
}
#endif
return ReportResult;
}
///
/// Check to see if we wish to reject a report based on the WER meta data.
///
/// A request containing the XML representation of a WERReportMetadata class instance.
/// true if we do not reject.
private CrashReporterResult CheckReportDetail( HttpListenerRequest Request )
{
CrashReporterResult ReportResult = new CrashReporterResult();
#if DISABLED_CRR
ReportResult.bSuccess = false;
ReportResult.Message = "CRR disabled";
CrashReporterReceiverServicer.WriteEvent("CheckReportDetail() Report rejected by disabled CRR");
#else
string WERReportMetadataString = GetContentStreamString( Request );
WERReportMetadata WERData = null;
if( WERReportMetadataString.Length > 0 )
{
try
{
WERData = XmlHandler.FromXmlString( WERReportMetadataString );
}
catch( System.Exception Ex )
{
CrashReporterReceiverServicer.WriteEvent( "Error during XmlHandler.FromXmlString, probably incorrect encoding, trying to fix: " + Ex.Message );
byte[] StringBytes = System.Text.Encoding.Unicode.GetBytes( WERReportMetadataString );
string ConvertedXML = System.Text.Encoding.UTF8.GetString( StringBytes );
WERData = XmlHandler.FromXmlString( ConvertedXML );
}
}
if( WERData != null )
{
// Ignore crashes in the minidump parser itself
ReportResult.bSuccess = true;
if( WERData.ProblemSignatures.Parameter0.ToLower() == "MinidumpDiagnostics".ToLower() )
{
ReportResult.bSuccess = false;
ReportResult.Message = "Rejecting MinidumpDiagnostics crash";
}
// Ignore Debug and DebugGame crashes
string CrashingModule = WERData.ProblemSignatures.Parameter3.ToLower();
if( CrashingModule.Contains( "-debug" ) )
{
ReportResult.bSuccess = false;
ReportResult.Message = "Rejecting Debug or DebugGame crash";
}
}
#endif
return ReportResult;
}
///
/// Receive a file and write it to a temporary folder.
///
/// A request containing the file details in the headers (DirectoryName/FileName/FileLength).
/// true if the file is received successfully.
/// There is an arbitrary file size limit of CrashReporterConstants.MaxFileSizeToUpload as a simple exploit prevention method.
private CrashReporterResult ReceiveFile( HttpListenerRequest Request )
{
CrashReporterResult ReportResult = new CrashReporterResult();
if( !Request.HasEntityBody )
{
return ReportResult;
}
// Take this opportunity to clean out folders for reports that were never completed
CheckForAbandonedReports();
// Make sure we have a sensible file size
long BytesToReceive = 0;
if (long.TryParse(Request.Headers["FileLength"], out BytesToReceive))
{
if (BytesToReceive >= CrashReporterConstants.MaxFileSizeToUpload)
{
return ReportResult;
}
}
string DirectoryName = Request.Headers["DirectoryName"];
string FileName = Request.Headers["FileName"];
var T = Request.ContentLength64;
bool bIsOverloaded = false;
if( !UploadsInProgress.TryReceiveFile( DirectoryName, FileName, BytesToReceive, ref ReportResult.Message, ref bIsOverloaded ) )
{
CrashReporterReceiverServicer.WriteEvent(ReportResult.Message);
ReportResult.bSuccess = false;
return ReportResult;
}
string PathName = Path.Combine( FileReceiptPath, DirectoryName, FileName );
// Recreate the file receipt directory, just in case.
Directory.CreateDirectory( FileReceiptPath );
// Create the folder to save files to
DirectoryInfo DirInfo = new DirectoryInfo( Path.GetDirectoryName( PathName ) );
DirInfo.Create();
// Make sure the file doesn't already exist. If it does, delete it.
if (File.Exists(PathName))
{
File.Delete(PathName);
}
FileInfo Info = new FileInfo( PathName );
FileStream FileWriter = Info.OpenWrite();
// Read in the input stream from the request, and write to a file
long OriginalBytesToReceive = BytesToReceive;
try
{
using (BinaryReader Reader = new BinaryReader(Request.InputStream))
{
byte[] Buffer = new byte[CrashReporterConstants.StreamChunkSize];
while (BytesToReceive > 0)
{
int BytesToRead = Math.Min((int)BytesToReceive, CrashReporterConstants.StreamChunkSize);
Interlocked.Add( ref UploadsInProgress.CurrentReceivedData, BytesToRead );
int ReceivedChunkSize = Reader.Read(Buffer, 0, BytesToRead);
if (ReceivedChunkSize == 0)
{
ReportResult.Message = string.Format("Partial file \"{0}\" received", FileName);
ReportResult.bSuccess = false;
CrashReporterReceiverServicer.WriteEvent(ReportResult.Message);
return ReportResult;
}
BytesToReceive -= ReceivedChunkSize;
FileWriter.Write(Buffer, 0, ReceivedChunkSize);
}
}
}
finally
{
FileWriter.Close();
Request.InputStream.Close();
UploadsInProgress.FileUploadAttemptDone( OriginalBytesToReceive, bIsOverloaded );
bool bWriteMetadata = Path.GetExtension( FileName ) == ".ue4crash";
if( bWriteMetadata )
{
string CompressedSize = Request.Headers["CompressedSize"];
string UncompressedSize = Request.Headers["UncompressedSize"];
string NumberOfFiles = Request.Headers["NumberOfFiles"];
string MetadataPath = Path.Combine( FileReceiptPath, DirectoryName, Path.GetFileNameWithoutExtension( FileName ) + ".xml" );
XmlHandler.WriteXml( new FCompressedCrashInformation( CompressedSize, UncompressedSize, NumberOfFiles ), MetadataPath );
}
}
ReportResult.bSuccess = true;
return ReportResult;
}
///
/// Rename to the temporary landing zone directory to the final location.
///
/// A request containing either the Report Id as a string or an XML representation of a CheckReportRequest class instance.
/// true if everything is renamed correctly.
private CrashReporterResult UploadComplete(HttpListenerRequest Request)
{
var ReportResult = new CrashReporterResult();
var RequestClass = new CheckReportRequest();
RequestClass.ReportId = GetReportIdFromPostData(GetContentStreamString(Request));
string IntermediatePathName = Path.Combine(FileReceiptPath, RequestClass.ReportId);
if (!UploadsInProgress.TrySetReportComplete(RequestClass.ReportId))
{
ReportResult.Message = string.Format("Report \"{0}\" has already been completed", RequestClass.ReportId);
ReportResult.bSuccess = false;
return ReportResult;
}
DirectoryInfo DirInfo = new DirectoryInfo(IntermediatePathName);
if (!DirInfo.Exists)
{
return ReportResult;
}
LandingZone.ReceiveReport(DirInfo, RequestClass.ReportId);
ReportResult.bSuccess = true;
int CurrentDay = DateTime.UtcNow.Day;
if( CurrentDay > LastDay )
{
// Check the log and create a new one for a new day.
CrashReporterReceiverServicer.Log.CreateNewLogFile();
LastDay = CurrentDay;
}
return ReportResult;
}
///
/// The main listener callback to handle client requests.
///
/// The request from the client.
private void AsyncHandleHttpRequest( IAsyncResult ClientRequest )
{
try
{
HttpListenerContext Context = ServiceHttpListener.EndGetContext( ClientRequest );
ServiceHttpListener.BeginGetContext( AsyncHandleHttpRequest, null );
HttpListenerRequest Request = Context.Request;
bool bIgnorePerfData = false;
using( HttpListenerResponse Response = Context.Response )
{
// Extract the URL parameters
string[] UrlElements = Request.RawUrl.Split( "/".ToCharArray(), StringSplitOptions.RemoveEmptyEntries );
// http://*:57005/CrashReporter/CheckReport
// http://*:57005/CrashReporter/CheckReportDetail
CrashReporterResult ReportResult = new CrashReporterResult();
if( UrlElements[0].ToLower() == "crashreporter" )
{
switch( UrlElements[1].ToLower() )
{
case "ping":
ReportResult.bSuccess = true;
break;
case "checkreport":
ReportResult = CheckReport( Context.Request );
break;
case "checkreportdetail":
ReportResult = CheckReportDetail( Context.Request );
break;
case "uploadreportfile":
ReportResult = ReceiveFile( Context.Request );
bIgnorePerfData = true;
break;
case "uploadcomplete":
ReportResult = UploadComplete( Context.Request );
break;
default:
ReportResult.bSuccess = false;
ReportResult.Message = "Invalid command: " + UrlElements[1];
break;
}
}
else
{
ReportResult.bSuccess = false;
ReportResult.Message = "Invalid application: " + UrlElements[0] + " (expecting CrashReporter)";
}
string ResponseString = XmlHandler.ToXmlString( ReportResult );
Response.SendChunked = true;
Response.ContentType = "text/xml";
byte[] Buffer = Encoding.UTF8.GetBytes( ResponseString );
Response.ContentLength64 = Buffer.Length;
Response.OutputStream.Write( Buffer, 0, Buffer.Length );
Response.StatusCode = ( int )HttpStatusCode.OK;
if( !bIgnorePerfData )
{
// Update the overhead data.
Int64 ContentLenght = Response.ContentLength64 + Request.ContentLength64;
Interlocked.Add( ref UploadsInProgress.CurrentReceivedData, ContentLenght );
}
}
}
catch( Exception Ex )
{
CrashReporterReceiverServicer.WriteEvent( "Error during async listen: " + Ex.Message );
}
}
///
/// Translate post data, which may be XML or a raw string, to a report ID.
///
/// A string that's either the Report Id or an XML representation of a CheckReportRequest class instance.
/// The report ID string
private string GetReportIdFromPostData(string ReportIdPostData)
{
if (ReportIdPostData.Length > 0)
{
// XML snippet will always start with <
if (ReportIdPostData[0] != '<')
{
return ReportIdPostData;
}
else
{
// Report id is embedded in a serialised request object (sent by old C# uploader)
var RequestObject = XmlHandler.FromXmlString(ReportIdPostData);
if ( RequestObject.ReportId != null && RequestObject.ReportId.Length > 0 )
{
return RequestObject.ReportId;
}
// LEGACY SUPPORT. ReportId was once named DirectoryName
else if ( RequestObject.DirectoryName != null && RequestObject.DirectoryName.Length > 0 )
{
return RequestObject.DirectoryName;
}
}
}
return "";
}
///
/// Periodically delete folders of abandoned reports
///
void CheckForAbandonedReports()
{
if (Interlocked.Exchange(ref CheckingForAbandonedReports, 1) == 1)
{
// Being checked by another thread, so no need
return;
}
try
{
var Now = DateTime.Now;
// No need to do this very often
if ((Now - LastAbandonedReportCheckTime).Minutes < MinimumMinutesBetweenAbandonedReportChecks)
{
return;
}
LastAbandonedReportCheckTime = Now;
try
{
foreach (string Dir in Directory.EnumerateDirectories(FileReceiptPath))
{
var LastAccessTime = new DateTime();
string ReportId = new DirectoryInfo(Dir).Name;
bool bGotAccessTime = UploadsInProgress.TryGetLastAccessTime(ReportId, ref LastAccessTime);
if (!bGotAccessTime || (Now - LastAccessTime).Minutes > AgeMinutesToConsiderReportAbandoned)
{
try
{
Directory.Delete(Dir, true /* delete all contents */);
}
catch (Exception Ex)
{
CrashReporterReceiverServicer.WriteEvent(string.Format("Failed to delete directory {0}: {1}", ReportId, Ex.Message));
}
}
}
}
catch (Exception Ex)
{
// If we get in here, the reports won't get cleaned up. May happen if
// permissions are incorrect or the directory is locked
CrashReporterReceiverServicer.WriteEvent( "Error during folder tidy: " + Ex.Message );
}
}
finally
{
// Allow other threads to check again
Interlocked.Exchange(ref CheckingForAbandonedReports, 0);
}
}
}
}