using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Net.Http.Headers; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Net; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace Nirvana.Common.File { public static class FileHelpers { // If you require a check on specific characters in the IsValidFileExtensionAndSignature // method, supply the characters in the _allowedChars field. private static readonly byte[] _allowedChars = { }; // For more file signatures, see the File Signatures Database (https://www.filesignatures.net/) // and the official specifications for the file types you wish to add. private static readonly Dictionary> _fileSignature = new Dictionary> { { ".gif", new List { new byte[] { 0x47, 0x49, 0x46, 0x38 } } }, { ".png", new List { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } } }, { ".jpeg", new List { new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 }, } }, { ".jpg", new List { new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 }, new byte[] { 0xFF, 0xD8, 0xFF, 0xE8 }, } }, { ".zip", new List { new byte[] { 0x50, 0x4B, 0x03, 0x04 }, new byte[] { 0x50, 0x4B, 0x4C, 0x49, 0x54, 0x45 }, new byte[] { 0x50, 0x4B, 0x53, 0x70, 0x58 }, new byte[] { 0x50, 0x4B, 0x05, 0x06 }, new byte[] { 0x50, 0x4B, 0x07, 0x08 }, new byte[] { 0x57, 0x69, 0x6E, 0x5A, 0x69, 0x70 }, } }, }; // **WARNING!** // In the following file processing methods, the file's content isn't scanned. // In most production scenarios, an anti-virus/anti-malware scanner API is // used on the file before making the file available to users or other // systems. For more information, see the topic that accompanies this sample // app. public static async Task ProcessFormFile(IFormFile formFile, ModelStateDictionary modelState, string[] permittedExtensions, long sizeLimit) { var fieldDisplayName = string.Empty; // Use reflection to obtain the display name for the model // property associated with this IFormFile. If a display // name isn't found, error messages simply won't show // a display name. MemberInfo property = typeof(T).GetProperty( formFile.Name.Substring(formFile.Name.IndexOf(".", StringComparison.Ordinal) + 1)); if (property != null) { if (property.GetCustomAttribute(typeof(DisplayAttribute)) is DisplayAttribute displayAttribute) { fieldDisplayName = $"{displayAttribute.Name} "; } } // Don't trust the file name sent by the client. To display // the file name, HTML-encode the value. var trustedFileNameForDisplay = WebUtility.HtmlEncode( formFile.FileName); // Check the file length. This check doesn't catch files that only have // a BOM as their content. if (formFile.Length == 0) { modelState.AddModelError(formFile.Name, $"{fieldDisplayName}({trustedFileNameForDisplay}) is empty."); return new byte[0]; } if (formFile.Length > sizeLimit) { var megabyteSizeLimit = sizeLimit / 1048576; modelState.AddModelError(formFile.Name, $"{fieldDisplayName}({trustedFileNameForDisplay}) exceeds " + $"{megabyteSizeLimit:N1} MB."); return new byte[0]; } try { using (var memoryStream = new MemoryStream()) { await formFile.CopyToAsync(memoryStream); // Check the content length in case the file's only // content was a BOM and the content is actually // empty after removing the BOM. if (memoryStream.Length == 0) { modelState.AddModelError(formFile.Name, $"{fieldDisplayName}({trustedFileNameForDisplay}) is empty."); } //if (!IsValidFileExtensionAndSignature( // formFile.FileName, memoryStream, permittedExtensions)) //{ // modelState.AddModelError(formFile.Name, // $"{fieldDisplayName}({trustedFileNameForDisplay}) file " + // "type isn't permitted or the file's signature " + // "doesn't match the file's extension."); //} //else //{ // return memoryStream.ToArray(); //} return memoryStream.ToArray(); } } catch (Exception ex) { modelState.AddModelError(formFile.Name, $"{fieldDisplayName}({trustedFileNameForDisplay}) upload failed. " + $"Please contact the Help Desk for support. Error: {ex.HResult}"); // Log the exception } return new byte[0]; } public static async Task ProcessStreamedFile( MultipartSection section, ContentDispositionHeaderValue contentDisposition, ModelStateDictionary modelState, string[] permittedExtensions, long sizeLimit) { try { using (var memoryStream = new MemoryStream()) { await section.Body.CopyToAsync(memoryStream); // Check if the file is empty or exceeds the size limit. if (memoryStream.Length == 0) { modelState.AddModelError("File", "文件不可为空"); } else if (memoryStream.Length > sizeLimit) { var megabyteSizeLimit = sizeLimit / 1048576; modelState.AddModelError("File", $"文件大小超过了{megabyteSizeLimit:N1} MB."); } else if (!IsValidFileExtensionAndSignature( contentDisposition.FileName.Value, memoryStream, permittedExtensions)) { modelState.AddModelError("File", "此文件格式不支持"); } else { return memoryStream.ToArray(); } } } catch (Exception ex) { modelState.AddModelError("File", $"上传失败,发生错误,请联系客服人员解决. Error: {ex.HResult}"); // Log the exception LogFactory.InsertErrorLog($"上传失败,{ex.Message},{ex.HResult}"); } return new byte[0]; } public static async Task ApiProcessStreamedFile( MultipartSection section, ContentDispositionHeaderValue contentDisposition, ModelStateDictionary modelState, string[] permittedExtensions, long sizeLimit) { try { using (var memoryStream = new MemoryStream()) { await section.Body.CopyToAsync(memoryStream); // Check if the file is empty or exceeds the size limit. if (memoryStream.Length == 0) { modelState.AddModelError("File", "文件不可为空"); } else if (memoryStream.Length > sizeLimit) { var megabyteSizeLimit = sizeLimit / 1048576; modelState.AddModelError("File", $"文件大小超过了{megabyteSizeLimit:N1} MB."); } else if (!IsValidFileExtensionAndSignature( contentDisposition.FileName.Value, memoryStream, permittedExtensions)) { modelState.AddModelError("File", "此文件格式不支持"); } else { return new ProcessData { data = memoryStream.ToArray(), modelState = modelState }; } } } catch (Exception ex) { modelState.AddModelError("File", $"上传失败,发生错误,请联系客服人员解决. Error: {ex.HResult}"); // Log the exception LogFactory.InsertErrorLog($"上传失败,{ex.Message},{ex.HResult}"); } return new ProcessData { data = new byte[0], modelState = modelState }; } public class ProcessData { public byte[] data { get; set; } public ModelStateDictionary modelState { get; set; } } private static bool IsValidFileExtensionAndSignature(string fileName, Stream data, string[] permittedExtensions) { if (string.IsNullOrEmpty(fileName) || data == null || data.Length == 0) { return false; } var ext = Path.GetExtension(fileName).ToLowerInvariant(); if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext)) { return false; } data.Position = 0; using (var reader = new BinaryReader(data)) { if (ext.Equals(".txt") || ext.Equals(".prn")) { if (_allowedChars.Length == 0) { // Limits characters to ASCII encoding. for (var i = 0; i < data.Length; i++) { var a = reader.ReadByte(); if (reader.ReadByte() > sbyte.MaxValue) { return false; } } } else { // Limits characters to ASCII encoding and // values of the _allowedChars array. for (var i = 0; i < data.Length; i++) { var b = reader.ReadByte(); if (b > sbyte.MaxValue || !_allowedChars.Contains(b)) { return false; } } } return true; } // Uncomment the following code block if you must permit // files whose signature isn't provided in the _fileSignature // dictionary. We recommend that you add file signatures // for files (when possible) for all file types you intend // to allow on the system and perform the file signature // check. if (!_fileSignature.ContainsKey(ext)) { return true; } // File signature check // -------------------- // With the file signatures provided in the _fileSignature // dictionary, the following code tests the input content's // file signature. //var signatures = _fileSignature[ext]; //var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length)); //return signatures.Any(signature => // headerBytes.Take(signature.Length).SequenceEqual(signature)); return true; } } } }