Files
UnrealEngineUWP/Engine/Source/Programs/CrashReporter/CrashReportReceiver/WebHandler.cs
Robert Manuszewski 301e5763c2 Copying //UE4/Dev-Core to //UE4/Dev-Main (Source: //UE4/Dev-Core @ 3228803)
#lockdown Nick.Penwarden
#rb none

==========================
MAJOR FEATURES + CHANGES
==========================

Change 3209807 on 2016/11/24 by Chris.Wood

	CRP now has improved handling of lost comms with website/DB (CRP v.1.2.9)
	[UE-38397] - CrashReportProcess should not lose crashes when the website goes down.

Change 3209936 on 2016/11/24 by Steven.Hutton

	Reconciled offline work. Fixed a number of display bugs with the reports page.

Change 3209938 on 2016/11/24 by Steven.Hutton

	Reconciled offline work

	Adding JQuery UI packages

Change 3210736 on 2016/11/28 by Steve.Robb

	Unset() made protected, which is an implementation details of TFunction and not the way to unbind one (may leak memory).
	Fixes to existing calls.

	https://answers.unrealengine.com/questions/494947/proper-way-to-deal-with-destroying-a-tfunction.html

Change 3211181 on 2016/11/28 by Steve.Robb

	Improved error message when binding a delegate to a pending kill object.

	#jira UE-5232

Change 3211214 on 2016/11/28 by Steve.Robb

	PR #2978: fixed typo in FMallocLeakDetection (Contributed by finap)

	#jira UE-39049

Change 3211301 on 2016/11/28 by Steve.Robb

	PR #2892: Copy bCustomVersionsAreReset when creating an Archive from another Ar. (Contributed by surakin)

	#jira UE-37941

Change 3213387 on 2016/11/29 by Steven.Hutton

	Test of a release note parsed from the changelist

Change 3213553 on 2016/11/29 by Gil.Gribb

	UE4 - Rework dependencies and UStruct creation so that we can create UBlueprintGeneratedClass's without needing the parent class serialized.

Change 3214800 on 2016/11/30 by Robert.Manuszewski

	ModuleManager will now use a critical section instead of FMultiReaderSingleWriterGT to make it really thread safe.

	- removed FMultiReaderSingleWriterGT because it was not fully thread safe

Change 3214926 on 2016/11/30 by Robert.Manuszewski

	Merging using Dev-Core_To_Dev-LoadTimes (reversed)

Change 3214981 on 2016/11/30 by Gil.Gribb

	UE4 - Make sure that subobjects of CDOs are exported even if they don't have any direct references; they might be archetypes.

Change 3215392 on 2016/11/30 by Steve.Robb

	Error out on editor builds when reading a non-boolean value in an archive.

Change 3215674 on 2016/11/30 by Steve.Robb

	Replacement of a custom scope-exit setup with our standard macro.

Change 3215720 on 2016/11/30 by Steve.Robb

	Default constructor for TTuple which value-initializes the elements.

Change 3216777 on 2016/12/01 by Graeme.Thornton

	Add NoRedist and NotForLicensees folders in the game folder to the C# version of config file hierarchy.

Change 3216858 on 2016/12/01 by Graeme.Thornton

	Improvements to pak signing and encryption
	 - Remove compile time defines for encryption key and signing keys
	 - Embed keys in executable by including in the UBT generated UELinkerFixups.cpp file
	 - Add key access core delegate for passing these keys through to pak platform file

Change 3216860 on 2016/12/01 by Graeme.Thornton

	Re-enable pak signing and encryption in ShooterGame

Change 3216861 on 2016/12/01 by Graeme.Thornton

	Re-enable pak signing and encryption in Paragon

Change 3217076 on 2016/12/01 by Gil.Gribb

	UE4 - replaced !GIsIntialLoad with EVENT_DRIVEN_ASYNC_LOAD_ACTIVE_AT_RUNTIME as a first step toward boot time EDL

Change 3221139 on 2016/12/05 by Ben.Woodhouse

	Dummy integrate of CL 3221131 (versioning workarounds) from Dev-LoadTimes4.14 with accept target to prevent those workarounds being merged out of dev-loadtimes4.14.

	Command:
	p4 integrate //UE4/Dev-LoadTimes4.14/...@3221131,3221131

	w/ resolve/accept-target

Change 3221410 on 2016/12/05 by Steve.Robb

	Allow Algo::IsSorted to work on C arrays and initializer_lists.

Change 3221673 on 2016/12/05 by Gil.Gribb

	set up an intentional merge conflict so we can do the right thing when dev-BP and dev-core come together

Change 3223182 on 2016/12/06 by Chris.Wood

	Add EngineModeEx to CrashReportCommon
	[UE-39258] - Update CrashReporter to include whether Engine crashes are from vanilla build or not

Change 3224646 on 2016/12/07 by Robert.Manuszewski

	It's no longer necessary to rebuild the cooked exe to enable the event driven loader. EDL and the new async IO are now controlled with ini settings only.

Change 3224647 on 2016/12/07 by Robert.Manuszewski

	Event Driven Loader is now enabled by default.

Change 3224669 on 2016/12/07 by Chris.Wood

	Compressing some crash files output from CRP to S3 (CRP v1.2.11)
	[UE-37564] - Add compression support for CRP crash files in S3

	Add EngineModeEx to support to CRP (CRP v1.2.10)
	[UE-39258] - Update CrashReporter to include whether Engine crashes are from vanilla build or not

Change 3224873 on 2016/12/07 by Robert.Manuszewski

	A few fixes to the EDL macro refactor.

Change 3224933 on 2016/12/07 by Robert.Manuszewski

	Async Loading settings will now be logged on startup in cooked games

Change 3224984 on 2016/12/07 by Robert.Manuszewski

	Small fix to EDL and new async IO global var init.

Change 3225027 on 2016/12/07 by Robert.Manuszewski

	Re-enabling EDL in ShooterGame since it no longer requires the cooked exe to be re-compiled (and EDL is on by default anyway).

Change 3226702 on 2016/12/08 by Steve.Robb

	UHT can now skip over macro-like identifiers while parsing.
	Correctly handles FTypefaceEntry::Name parsing, which was broken by the workaround in CL# 3219332.
	Syntax workaround reverted in FTypefaceEntry.

	#jira UE-38231

Change 3226756 on 2016/12/08 by Steve.Robb

	LexicalConversion renamed to Lex after #core discussion.
	Lex::ToString overloads added for FString itself.

Change 3226766 on 2016/12/08 by Steve.Robb

	FCookStatsManager::ToString replaced with Lex::ToString, as it now supports the required functionality.

Change 3226949 on 2016/12/08 by Robert.Manuszewski

	Fixing static analysis warnings

Change 3228566 on 2016/12/09 by Robert.Manuszewski

	Making UBT not think it needs to use separate temporary target for non source code projects when EDL settings don't match default.

Change 3228601 on 2016/12/09 by Steve.Robb

	Fix for TSparseArray visualization of types smaller than 8 bytes.

Change 3228654 on 2016/12/09 by Gil.Gribb

	UE4 - Fix very old bug with lock free lists, probably not relevant to anything we currently use.

Change 3228697 on 2016/12/09 by Graeme.Thornton

	Rebuilt UnrealPak

[CL 3228932 by Robert Manuszewski in Main branch]
2016-12-09 11:36:14 -05:00

545 lines
17 KiB
C#

// 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
{
/// <summary>
/// A class to handle the receiving of crash reports.
/// </summary>
public class WebHandler : IDisposable
{
/// <summary>A flag to check to ensure the service started properly.</summary>
public bool bStartedSuccessfully = false;
/// <summary>Base http listener to which the async listeners are associated.</summary>
private HttpListener ServiceHttpListener = null;
/// <summary>Intermediate path crash reports are downloaded to until complete.</summary>
private string FileReceiptPath;
/// <summary>Object to take care of incoming files.</summary>
LandingZoneMonitor LandingZone;
/// <summary>Synchronisation object to cope with erratic client behaviour.</summary>
FUploadsInProgress UploadsInProgress = new FUploadsInProgress();
/// <summary>Leave at least this amount of time to leave between checks for abandoned reports</summary>
const int MinimumMinutesBetweenAbandonedReportChecks = 30;
/// <summary>Once a report is this old without being completed, it gets deleted</summary>
const int AgeMinutesToConsiderReportAbandoned = 4*60;
/// <summary>Time of most recent check for abandoned incomplete reports</summary>
DateTime LastAbandonedReportCheckTime = DateTime.Now;
/// <summary> Last day, used to create a new log file. </summary>
int LastDay = DateTime.UtcNow.Day;
/// <summary>Atomic flag to indicate a thread is checking for abandoned reports</summary>
int CheckingForAbandonedReports = 0;
/// <summary>
/// Implementing Dispose.
/// </summary>
public void Dispose()
{
Dispose( true );
GC.SuppressFinalize( this );
}
/// <summary>
/// Disposes the resources.
/// </summary>
/// <param name="Disposing">true if the Dispose call is from user code, and not system code.</param>
protected virtual void Dispose( bool Disposing )
{
if (UploadsInProgress != null)
{
UploadsInProgress.Dispose();
UploadsInProgress = null;
}
if (ServiceHttpListener != null)
{
ServiceHttpListener.Close();
}
}
/// <summary>
/// Initialise the service to listen for reports.
/// </summary>
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() );
}
}
/// <summary>
/// Cleanup any used resources.
/// </summary>
public void Release()
{
if( bStartedSuccessfully )
{
if( ServiceHttpListener != null )
{
ServiceHttpListener.Stop();
ServiceHttpListener.Abort();
}
}
}
/// <summary>
/// Read in the web request payload as a string.
/// </summary>
/// <param name="Request">A listener request.</param>
/// <returns>A string of the input stream in the requests encoding.</returns>
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;
}
/// <summary>
/// Check to see if a report has already been uploaded.
/// </summary>
/// <param name="Request">A request containing either the Report Id as a string or an XML representation of a CheckReportRequest class instance.</param>
/// <returns>Result object, indicating whether the report has already been uploaded.</returns>
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;
}
/// <summary>
/// Check to see if we wish to reject a report based on the WER meta data.
/// </summary>
/// <param name="Request">A request containing the XML representation of a WERReportMetadata class instance.</param>
/// <returns>true if we do not reject.</returns>
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<WERReportMetadata>( 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<WERReportMetadata>( 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;
}
/// <summary>
/// Receive a file and write it to a temporary folder.
/// </summary>
/// <param name="Request">A request containing the file details in the headers (DirectoryName/FileName/FileLength).</param>
/// <returns>true if the file is received successfully.</returns>
/// <remarks>There is an arbitrary file size limit of CrashReporterConstants.MaxFileSizeToUpload as a simple exploit prevention method.</remarks>
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<FCompressedCrashInformation>( new FCompressedCrashInformation( CompressedSize, UncompressedSize, NumberOfFiles ), MetadataPath );
}
}
ReportResult.bSuccess = true;
return ReportResult;
}
/// <summary>
/// Rename to the temporary landing zone directory to the final location.
/// </summary>
/// <param name="Request">A request containing either the Report Id as a string or an XML representation of a CheckReportRequest class instance.</param>
/// <returns>true if everything is renamed correctly.</returns>
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;
}
/// <summary>
/// The main listener callback to handle client requests.
/// </summary>
/// <param name="ClientRequest">The request from the client.</param>
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<CrashReporterResult>( 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 );
}
}
/// <summary>
/// Translate post data, which may be XML or a raw string, to a report ID.
/// </summary>
/// <param name="ReportIdPostData">A string that's either the Report Id or an XML representation of a CheckReportRequest class instance.</param>
/// <returns>The report ID string</returns>
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<CheckReportRequest>(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 "";
}
/// <summary>
/// Periodically delete folders of abandoned reports
/// </summary>
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);
}
}
}
}