private StreamWriteStatus BufferStreamToResponse(StreamLoaderResult fileStreamDetails, IEnumerable <RangeRequest> rangeRequests, string contentType) { bool isMultipart = RangeRequestHelpers.IsMultipartRequest(rangeRequests); int bytesRead; long bytesToRead; long remainingBytes = fileStreamDetails.Metadata.Size.Value; byte[] buffer = new byte[this._bufferSizeBytes]; TextWriter _output = this._response.Output; //stream each requested range to the client foreach (RangeRequest rangeRequest in rangeRequests) { fileStreamDetails.FileStream.Seek(rangeRequest.Start, SeekOrigin.Begin); bytesToRead = rangeRequest.End - rangeRequest.Start + 1; // If this is a multipart response, we must add certain headers before streaming the content: if (isMultipart) { _output.Write(rangeRequest.GetMultipartIntermediateHeader(contentType)); } while (bytesToRead > 0) { if (!this._response.IsClientConnected) { return(StreamWriteStatus.ClientDisconnected); } //buffer length can only be a max of int32 in length -- so we know that this value will always be less than an int32 int toRead = Convert.ToInt32(Math.Min(Convert.ToInt64(buffer.Length), bytesToRead)); bytesRead = fileStreamDetails.FileStream.Read(buffer, 0, toRead); this._response.OutputStream.Write(buffer, 0, bytesRead); this._response.Flush(); bytesToRead -= bytesRead; } // In Multipart responses, mark the end of the part if (isMultipart) { _output.WriteLine(); } } if (isMultipart) { _output.Write(_MultipartFooter); } return(StreamWriteStatus.ApplicationRestarted); }
private bool HeadersValidate(StreamLoaderResult fileDetails, out IEnumerable <RangeRequest> rangeRequests) { rangeRequests = null; //we have ranges specified in the header, but they're invalid if (this._request.HasRangeHeaders() && !this._request.TryParseRangeRequests(fileDetails.Metadata.Size.Value, out rangeRequests)) { RedirectWithStatusCode(this._response, HttpStatusCode.BadRequest, _configuration.UnauthorizedErrorRedirectUrl); return(false); } //modified since the requested date -- passing this sessionType value back will keep everything behind the scenes w/ the browser else if (fileDetails.Metadata.LastWriteTimeUtc.HasValue && !this._request.IfModifiedSinceHeaderIsBeforeGiven(fileDetails.Metadata.LastWriteTimeUtc.Value)) { this._response.StatusCode = (int)HttpStatusCode.NotModified; return(false); } else if (fileDetails.Metadata.LastWriteTimeUtc.HasValue && !this._request.IfUnmodifiedHeaderIsAfterGivenDate(fileDetails.Metadata.LastWriteTimeUtc.Value)) { this._response.StatusCode = (int)HttpStatusCode.PreconditionFailed; return(false); } //ETag doesn't match request else if (!this._request.IfMatchHeaderIsValid(fileDetails.Metadata.ExpectedMD5)) { this._response.StatusCode = (int)HttpStatusCode.PreconditionFailed; return(false); } // There is an If-None-Match header with "*" specified, which we don't allow else if (this._request.IfNoneMatchHeaderIsWildcard()) { this._response.StatusCode = (int)HttpStatusCode.PreconditionFailed; return(false); } // The entity does match the none-match request, the response code was set inside the IsIfNoneMatchHeaderValid function else if (this._request.IfNoneMatchHeaderHasMatchingETagSpecified(fileDetails.Metadata.ExpectedMD5)) { this._response.StatusCode = (int)HttpStatusCode.NotModified; this._response.AppendHeader(HttpHeaderFields.EntityTag.ToEnumValueString(), fileDetails.Metadata.ExpectedMD5); return(false); } return(true); }
private void RedirectOnFailedStatus(StreamLoaderResult fileDetails) { switch (fileDetails.Status) { case StreamLoadStatus.FileStoredInCloud: _statusLogger.Log(this._context, StreamWriteStatus.RedirectedClientToCloudUri); RedirectWithStatusCode(this._response, HttpStatusCode.MovedPermanently, fileDetails.CloudLocation.ToUrl()); return; case StreamLoadStatus.UnauthorizedAccess: _statusLogger.Log(this._context, StreamWriteStatus.UnauthorizedAccessError); RedirectWithStatusCode(this._response, HttpStatusCode.BadRequest, _configuration.UnauthorizedErrorRedirectUrl); return; //TODO: these 2 might be the same scenario case StreamLoadStatus.InvalidRequest: _statusLogger.Log(this._context, StreamWriteStatus.UserRequestError); RedirectWithStatusCode(this._response, HttpStatusCode.BadRequest, _configuration.UnauthorizedErrorRedirectUrl); return; case StreamLoadStatus.FileMetadataNotFound: _statusLogger.Log(this._context, StreamWriteStatus.UserRequestError); RedirectWithStatusCode(this._response, HttpStatusCode.BadRequest, _configuration.UnauthorizedErrorRedirectUrl); return; case StreamLoadStatus.FileSizeMismatched: _statusLogger.Log(this._context, StreamWriteStatus.MismatchedSizeError); RedirectWithStatusCode(this._response, HttpStatusCode.InternalServerError, _configuration.ServerErrorUrl); return; case StreamLoadStatus.MD5Failed: _statusLogger.Log(this._context, StreamWriteStatus.MD5Failed); //log.Error(String.Format("File Corrupt -- Failed MD5 check - Identity [{0}] - FileId [{1}] - User Id [{2}] / Name [{3}] / Email [{4}]{5}File Path [ {6} ]{5}{5}{7}", WindowsIdentity.GetCurrent().Name, FileId, context.OnyxUser().Identity.OCPUser.Individual.OnyxId, context.OnyxUser().Identity.OCPUser.Individual.UserName, context.OnyxUser().Identity.OCPUser.Individual.UserEmailAddress, Environment.NewLine, properties.MappedPath, this._request.ToLogString())); RedirectWithStatusCode(this._response, HttpStatusCode.InternalServerError, _configuration.ServerErrorUrl); return; case StreamLoadStatus.FileNotFound: _statusLogger.Log(this._context, StreamWriteStatus.NotFound); //log.Error(String.Format("File Not Found - Identity [{0}] - FileId [{1}] - User Id [{2}] / Name [{3}] / Email [{4}]{5}File Path [ {6} ]{5}{5}{7}", WindowsIdentity.GetCurrent().Name, FileId, context.OnyxUser().Identity.OCPUser.Individual.OnyxId, context.OnyxUser().Identity.OCPUser.Individual.UserName, context.OnyxUser().Identity.OCPUser.Individual.UserEmailAddress, Environment.NewLine, properties.MappedPath, this._request.ToLogString())); RedirectWithStatusCode(this._response, HttpStatusCode.InternalServerError, _configuration.ServerErrorUrl); return; } }
/// <summary> Stream file. </summary> /// <remarks> ebrown, 2/15/2011. </remarks> /// <exception cref="ArgumentNullException"> Thrown when one or more required arguments are null. </exception> /// <exception cref="Exception"> Thrown when exception. </exception> /// <param name="requestedResponseType"> Type of the requested response. </param> /// <param name="downloadProperties"> The download properties. </param> /// <param name="rangeRequests"> The range requests. </param> /// <param name="ifRangeEntityTag"> if range entity tag. </param> /// <param name="forceDownload"> true to force download. </param> /// <returns> . </returns> public StreamWriteStatus StreamFile(HttpResponseType requestedResponseType, StreamLoaderResult downloadProperties, IEnumerable <RangeRequest> rangeRequests, string ifRangeEntityTag, bool forceDownload) { if (null == downloadProperties) { throw new ArgumentNullException("downloadProperties"); } if (null == rangeRequests) { throw new ArgumentNullException("rangeRequests"); } try { string contentType = !string.IsNullOrEmpty(downloadProperties.ContentType) ? downloadProperties.ContentType : MimeTypes.GetMimeTypeForFileExtension(Path.GetExtension(downloadProperties.Metadata.FileName)); if (forceDownload) { this._response.AppendHeader("content-disposition", String.Format(CultureInfo.InvariantCulture, "attachment; filename={0}", downloadProperties.Metadata.FileName)); } bool isMultipart = RangeRequestHelpers.IsMultipartRequest(rangeRequests), isActionableRangeRequest = IsActionableRangeRequest(downloadProperties.Metadata, rangeRequests, ifRangeEntityTag); //TODO: is this response.Clear() necessary here? this._response.Clear(); long responseContentLength = CalculateContentLength(downloadProperties.Metadata, rangeRequests, ifRangeEntityTag, contentType, isMultipart); this._response.StatusCode = isActionableRangeRequest ? (int)HttpStatusCode.PartialContent : (int)HttpStatusCode.OK; if (isActionableRangeRequest && !isMultipart) { var first = rangeRequests.First(); //must indicate the Response Range of in the initial HTTP Header since this isn't multipart this._response.AppendHeader(HttpHeaderFields.ContentRange.ToEnumValueString(), String.Format(CultureInfo.InvariantCulture, "bytes {0}-{1}/{2}", first.Start, first.End, downloadProperties.Metadata.Size.Value)); } this._response.AppendHeader(HttpHeaderFields.ContentLength.ToEnumValueString(), responseContentLength.ToString(CultureInfo.InvariantCulture)); //don't go off the DB insert date for last modified b/c the file system file could be older (b/c multiple files records inserted at different dates could share the same physical file) if (downloadProperties.Metadata.LastWriteTimeUtc.HasValue) { this._response.AppendHeader(HttpHeaderFields.LastModified.ToEnumValueString(), downloadProperties.Metadata.LastWriteTimeUtc.Value.ToString("r", CultureInfo.InvariantCulture)); } this._response.AppendHeader(HttpHeaderFields.AcceptRanges.ToEnumValueString(), "bytes"); this._response.AppendHeader(HttpHeaderFields.EntityTag.ToEnumValueString(), String.Format(CultureInfo.InvariantCulture, "\"{0}\"", downloadProperties.Metadata.ExpectedMD5)); //not sure if we should use the Keep-Alive header? //this._response.AppendHeader(HttpHeaderFields.HTTP_HEADER_KEEP_ALIVE, "timeout=15, max=30"); //multipart messages are special -> file's actual mime type written into Response later this._response.ContentType = (isMultipart ? MultipartNames.MultipartContentType : contentType); //we've dumped our HEAD and can return now if (HttpResponseType.HeadOnly == requestedResponseType) { // Flush the HEAD information to the client... this._response.Flush(); return(StreamWriteStatus.SentHttpHead); } StreamWriteStatus result = BufferStreamToResponse(downloadProperties, rangeRequests, contentType); return((StreamWriteStatus.ClientDisconnected == result) ? result : StreamWriteStatus.SentFile); //this causes a ThreadAbortException //Response.End(); } catch (IOException ex) { string userName = string.Empty; using (var identity = WindowsIdentity.GetCurrent()) { userName = identity.Name; } //, this._request.ToLogString() log.Error(String.Format(CultureInfo.InvariantCulture, "File read failure for user {0}", userName), ex); return(StreamWriteStatus.StreamReadError); } catch (Exception ex) { HttpException httpEx = ex as HttpException; //suck up the remote connection failure if ((null != httpEx) && (HttpExceptionErrorCodes.ConnectionAborted == httpEx.ErrorCode)) { return(StreamWriteStatus.ClientDisconnected); } //bubble up to the caller, and let them log it -- this maintains the identity of our original exception as the innerexception throw; //log.Error("Unexpected failure for " + WindowsIdentity.GetCurrent().Name, ex); //error = "Unexpected File Transfer Error - FileId [" + FileId.ToString() + "] - User Id [" + context.OnyxUser().Identity.OCPUser.Individual.OnyxId.ToString() + "] / Name [" + ((userProperties.Name == null) ? string.Empty : userProperties.Name) + "] / Email [" + ((userProperties.Email == null) ? string.Empty : userProperties.Email) + "]" + Environment.NewLine + "File Path [ " + properties.MappedPath + " ]"; //return DownloadFileStatus.UnexpectedError; } }
/// <summary> /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler" /// />interface. /// </summary> /// <remarks> ebrown, 2/10/2011. </remarks> /// <exception cref="ObjectDisposedException"> Thrown when a supplied object has been disposed. </exception> /// <exception cref="ArgumentNullException"> Thrown when one or more required arguments are null. </exception> /// <param name="context"> An <see cref="T:System.Web.HttpContext" /> object that provides references to the intrinsic server objects /// (for example, Request, Response, Session, and Server) used to service HTTP requests. </param> public override void ProcessRequest(HttpContextBase context) { if (_disposed) { throw new ObjectDisposedException("this"); } stopwatch.Start(); //TODO: 5-1-09 -- Response.Redirect doesn't make any sense... maybe we should just give them an 'unauthorized' empty file thing if (null == context) { throw new ArgumentNullException("context"); } this._context = context; //assign to local fields for micro-perf since these are virtuals this._request = context.Request; this._response = context.Response; try { var method = this._request.HttpMethod; //we only accept GET or HEAD - don't log these if ((HttpMethodNames.Get.ToEnumValueString() != method) && (HttpMethodNames.Header.ToEnumValueString() != method)) { this._response.StatusCode = (int)HttpStatusCode.NotImplemented; this._response.Redirect(_configuration.UnauthorizedErrorRedirectUrl, false); return; } _statusLogger.Log(context, StreamWriteStatus.ApplicationRestarted); //now pass the HttpRequestBase to our class responsible for examining specific state and loading the Stream using (StreamLoaderResult fileDetails = _streamLoader.ParseRequest(this._request)) { if (fileDetails.Status != StreamLoadStatus.Success) { RedirectOnFailedStatus(fileDetails); return; } IEnumerable <RangeRequest> rangeRequests; if (!HeadersValidate(fileDetails, out rangeRequests)) { return; } HttpResponseType responseStreamType = method == HttpMethodNames.Header.ToEnumValueString() ? HttpResponseType.HeadOnly : HttpResponseType.Complete; //we've passed our credential checking and we're ready to send out our response StreamWriteStatus streamWriteStatus = new ResponseStreamWriter(this._response, this._configuration.FileTransferBufferSizeInKBytes) .StreamFile(responseStreamType, fileDetails, rangeRequests, this._request.RetrieveHeader(HttpHeaderFields.IfRange), true); HandleStreamWriteStatus(streamWriteStatus); } } catch (HttpException hex) { //remote host closed the connection if (HttpExceptionErrorCodes.ConnectionAborted == hex.ErrorCode) { //log.Warn(String.Format("Remote Host Closed Transfer - Identity [{0}] - FileId [{1}] - User Id [{2}] / Name [{3}] / Email [{4}]{5}{5}{6}", WindowsIdentity.GetCurrent().Name, FileId, context.OnyxUser().Identity.OCPUser.Individual.OnyxId, context.OnyxUser().Identity.OCPUser.Individual.UserName, context.OnyxUser().Identity.OCPUser.Individual.UserEmailAddress, Environment.NewLine, this._request.ToLogString()), ex); } } //eat it -- nothing we can do catch (ThreadAbortException) { } //log.Error(String.Format("Unexpected Download Exception Occurred - Identity [{0}] - FileId [{1}] - User Id [{2}] / Name [{3}] / Email [{4}]{5}{5}{6}", WindowsIdentity.GetCurrent().Name, FileId, context.OnyxUser().Identity.OCPUser.Individual.OnyxId, context.OnyxUser().Identity.OCPUser.Individual.UserName, context.OnyxUser().Identity.OCPUser.Individual.UserEmailAddress, Environment.NewLine, this._request.ToLogString()), ex); //SafeInternalServerError(context); }
private bool HeadersValidate(StreamLoaderResult fileDetails, out IEnumerable<RangeRequest> rangeRequests) { rangeRequests = null; //we have ranges specified in the header, but they're invalid if (this._request.HasRangeHeaders() && !this._request.TryParseRangeRequests(fileDetails.Metadata.Size.Value, out rangeRequests)) { RedirectWithStatusCode(this._response, HttpStatusCode.BadRequest, _configuration.UnauthorizedErrorRedirectUrl); return false; } //modified since the requested date -- passing this sessionType value back will keep everything behind the scenes w/ the browser else if (fileDetails.Metadata.LastWriteTimeUtc.HasValue && !this._request.IfModifiedSinceHeaderIsBeforeGiven(fileDetails.Metadata.LastWriteTimeUtc.Value)) { this._response.StatusCode = (int)HttpStatusCode.NotModified; return false; } else if (fileDetails.Metadata.LastWriteTimeUtc.HasValue && !this._request.IfUnmodifiedHeaderIsAfterGivenDate(fileDetails.Metadata.LastWriteTimeUtc.Value)) { this._response.StatusCode = (int)HttpStatusCode.PreconditionFailed; return false; } //ETag doesn't match request else if (!this._request.IfMatchHeaderIsValid(fileDetails.Metadata.ExpectedMD5)) { this._response.StatusCode = (int)HttpStatusCode.PreconditionFailed; return false; } // There is an If-None-Match header with "*" specified, which we don't allow else if (this._request.IfNoneMatchHeaderIsWildcard()) { this._response.StatusCode = (int)HttpStatusCode.PreconditionFailed; return false; } // The entity does match the none-match request, the response code was set inside the IsIfNoneMatchHeaderValid function else if (this._request.IfNoneMatchHeaderHasMatchingETagSpecified(fileDetails.Metadata.ExpectedMD5)) { this._response.StatusCode = (int)HttpStatusCode.NotModified; this._response.AppendHeader(HttpHeaderFields.EntityTag.ToEnumValueString(), fileDetails.Metadata.ExpectedMD5); return false; } return true; }
/// <summary> Stream file. </summary> /// <remarks> ebrown, 2/15/2011. </remarks> /// <exception cref="ArgumentNullException"> Thrown when one or more required arguments are null. </exception> /// <exception cref="Exception"> Thrown when exception. </exception> /// <param name="requestedResponseType"> Type of the requested response. </param> /// <param name="downloadProperties"> The download properties. </param> /// <param name="rangeRequests"> The range requests. </param> /// <param name="ifRangeEntityTag"> if range entity tag. </param> /// <param name="forceDownload"> true to force download. </param> /// <returns> . </returns> public StreamWriteStatus StreamFile(HttpResponseType requestedResponseType, StreamLoaderResult downloadProperties, IEnumerable<RangeRequest> rangeRequests, string ifRangeEntityTag, bool forceDownload) { if (null == downloadProperties) { throw new ArgumentNullException("downloadProperties"); } if (null == rangeRequests) { throw new ArgumentNullException("rangeRequests"); } try { string contentType = !string.IsNullOrEmpty(downloadProperties.ContentType) ? downloadProperties.ContentType : MimeTypes.GetMimeTypeForFileExtension(Path.GetExtension(downloadProperties.Metadata.FileName)); if (forceDownload) { this._response.AppendHeader("content-disposition", String.Format(CultureInfo.InvariantCulture, "attachment; filename={0}", downloadProperties.Metadata.FileName)); } bool isMultipart = RangeRequestHelpers.IsMultipartRequest(rangeRequests), isActionableRangeRequest = IsActionableRangeRequest(downloadProperties.Metadata, rangeRequests, ifRangeEntityTag); //TODO: is this response.Clear() necessary here? this._response.Clear(); long responseContentLength = CalculateContentLength(downloadProperties.Metadata, rangeRequests, ifRangeEntityTag, contentType, isMultipart); this._response.StatusCode = isActionableRangeRequest ? (int)HttpStatusCode.PartialContent : (int)HttpStatusCode.OK; if (isActionableRangeRequest && !isMultipart) { var first = rangeRequests.First(); //must indicate the Response Range of in the initial HTTP Header since this isn't multipart this._response.AppendHeader(HttpHeaderFields.ContentRange.ToEnumValueString(), String.Format(CultureInfo.InvariantCulture, "bytes {0}-{1}/{2}", first.Start, first.End, downloadProperties.Metadata.Size.Value)); } this._response.AppendHeader(HttpHeaderFields.ContentLength.ToEnumValueString(), responseContentLength.ToString(CultureInfo.InvariantCulture)); //don't go off the DB insert date for last modified b/c the file system file could be older (b/c multiple files records inserted at different dates could share the same physical file) if (downloadProperties.Metadata.LastWriteTimeUtc.HasValue) this._response.AppendHeader(HttpHeaderFields.LastModified.ToEnumValueString(), downloadProperties.Metadata.LastWriteTimeUtc.Value.ToString("r", CultureInfo.InvariantCulture)); this._response.AppendHeader(HttpHeaderFields.AcceptRanges.ToEnumValueString(), "bytes"); this._response.AppendHeader(HttpHeaderFields.EntityTag.ToEnumValueString(), String.Format(CultureInfo.InvariantCulture, "\"{0}\"", downloadProperties.Metadata.ExpectedMD5)); //not sure if we should use the Keep-Alive header? //this._response.AppendHeader(HttpHeaderFields.HTTP_HEADER_KEEP_ALIVE, "timeout=15, max=30"); //multipart messages are special -> file's actual mime type written into Response later this._response.ContentType = (isMultipart ? MultipartNames.MultipartContentType : contentType); //we've dumped our HEAD and can return now if (HttpResponseType.HeadOnly == requestedResponseType) { // Flush the HEAD information to the client... this._response.Flush(); return StreamWriteStatus.SentHttpHead; } StreamWriteStatus result = BufferStreamToResponse(downloadProperties, rangeRequests, contentType); return (StreamWriteStatus.ClientDisconnected == result) ? result : StreamWriteStatus.SentFile; //this causes a ThreadAbortException //Response.End(); } catch (IOException ex) { string userName = string.Empty; using (var identity = WindowsIdentity.GetCurrent()) { userName = identity.Name; } //, this._request.ToLogString() log.Error(String.Format(CultureInfo.InvariantCulture, "File read failure for user {0}", userName), ex); return StreamWriteStatus.StreamReadError; } catch (Exception ex) { HttpException httpEx = ex as HttpException; //suck up the remote connection failure if ((null != httpEx) && (HttpExceptionErrorCodes.ConnectionAborted == httpEx.ErrorCode)) return StreamWriteStatus.ClientDisconnected; //bubble up to the caller, and let them log it -- this maintains the identity of our original exception as the innerexception throw; //log.Error("Unexpected failure for " + WindowsIdentity.GetCurrent().Name, ex); //error = "Unexpected File Transfer Error - FileId [" + FileId.ToString() + "] - User Id [" + context.OnyxUser().Identity.OCPUser.Individual.OnyxId.ToString() + "] / Name [" + ((userProperties.Name == null) ? string.Empty : userProperties.Name) + "] / Email [" + ((userProperties.Email == null) ? string.Empty : userProperties.Email) + "]" + Environment.NewLine + "File Path [ " + properties.MappedPath + " ]"; //return DownloadFileStatus.UnexpectedError; } }
private StreamWriteStatus BufferStreamToResponse(StreamLoaderResult fileStreamDetails, IEnumerable<RangeRequest> rangeRequests, string contentType) { bool isMultipart = RangeRequestHelpers.IsMultipartRequest(rangeRequests); int bytesRead; long bytesToRead; long remainingBytes = fileStreamDetails.Metadata.Size.Value; byte[] buffer = new byte[this._bufferSizeBytes]; TextWriter _output = this._response.Output; //stream each requested range to the client foreach (RangeRequest rangeRequest in rangeRequests) { fileStreamDetails.FileStream.Seek(rangeRequest.Start, SeekOrigin.Begin); bytesToRead = rangeRequest.End - rangeRequest.Start + 1; // If this is a multipart response, we must add certain headers before streaming the content: if (isMultipart) { _output.Write(rangeRequest.GetMultipartIntermediateHeader(contentType)); } while (bytesToRead > 0) { if (!this._response.IsClientConnected) { return StreamWriteStatus.ClientDisconnected; } //buffer length can only be a max of int32 in length -- so we know that this value will always be less than an int32 int toRead = Convert.ToInt32(Math.Min(Convert.ToInt64(buffer.Length), bytesToRead)); bytesRead = fileStreamDetails.FileStream.Read(buffer, 0, toRead); this._response.OutputStream.Write(buffer, 0, bytesRead); this._response.Flush(); bytesToRead -= bytesRead; } // In Multipart responses, mark the end of the part if (isMultipart) { _output.WriteLine(); } } if (isMultipart) { _output.Write(_MultipartFooter); } return StreamWriteStatus.ApplicationRestarted; }