دانلود و بارگذاری فایل، Streaming (جریان‌دهی) و CORS

دانلود و بارگذاری فایل، Streaming (جریان‌دهی) و CORS

دانلود و بارگذاری فایل، Streaming (جریان‌دهی) و CORS

منبع: Coding Clean, Reliable, and Safe REST APIs with ASP.NET Core 8 — Anthony Giretti

اعتبار ترجمه: ترجمه با کمک هوش مصنوعی

Download و Upload فایل، Streaming و CORS

پایان CountryPatchValidator

           }
             });
        }
    }

مثال‌های CRUD قبلی Happy Path را نشان می‌دادند. در Application واقعی Error Management پیچیده‌تر است و در فصل بعد روش‌های سراسری‌تر مدیریت خطا بررسی می‌شوند.

Download و Upload فایل

مدیریت File یکی از کارهای رایج Developer است. هرچند Download/Upload را می‌توان بخشی از CRUD دانست، به دلیل نکات متعدد در بخش مستقلی بررسی می‌شوند.

Downloading Files

برای Download File سه اطلاعات لازم است:

  1. MIME type فایل.
  2. محتوای فایل به شکل Byte Array.
  3. نام فایل.

سپس از Method برابر Results.File استفاده می‌کنیم. MIME typeها بسیار متنوع‌اند و Internet Assigned Numbers Authority (IANA) فهرست استاندارد آن‌ها را نگهداری می‌کند.

برای مثال، ICountryService با Methodای به نام GetFile گسترش داده می‌شود.

Listing 4-27. ICountryService با GetFile

using Domain.DTOs;

    namespace Domain.Services;

    public interface ICountryService
    {
        CountryDto Retrieve(int id);
        List<CountryDto> GetAll();
        int CreateOrUpdate(CountryDto country);
        bool UpdateDescription(int id, string description);
        bool Delete(int id);
        (
            byte[] fileContent,
            string mimeType,
    
            string filename
        ) GetFile();
    }

Tuple سه مقدار لازم را برمی‌گرداند: Byte Array محتوا، MIME type و Filename.

Listing 4-28. GET /countries/download

app.MapGet("/countries/download", (
               ICountryService countryService) => {

        (
            byte[] fileContent,
            string mimeType,
            string fileName) = countryService.GetFile();

        if (fileContent is null || mimeType is null)
            return Results.NotFound();

        return Results.File(fileContent, mimeType, fileName);
    });

Endpoint دادهٔ لازم را از Service می‌گیرد؛ اگر Content یا MIME type وجود نداشته باشد 404 و در غیر این صورت File Response می‌دهد.

تصویر منبع — صفحهٔ 165Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 165.
تصویر منبع — صفحهٔ 165Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 165.
شکل ۴-۳۱. فراخوانی GET /countries/download

در نمونه Filename برابر countries.csv و MIME type برابر text/csv است.

شکل ۴-۳۲. Response دانلود countries.csv در Postman

Postman اگر بتواند File را تفسیر کند، Content را نمایش می‌دهد؛ مرورگر File را طبق Headerها Download می‌کند.

تصویر منبع — صفحهٔ 166Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 166.
تصویر منبع — صفحهٔ 166Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 166.
شکل ۴-۳۳. Download فایل countries.csv در Browser
شکل ۴-۳۴. Headerهای Download شامل Content-Length، Content-Type و Content-Disposition

Uploading Files

Upload کمی پیچیده‌تر است، زیرا Integrity و Security فایل باید Validate شود. دو Scenario اصلی داریم:

  1. یک یا چند File بدون Payload اضافی.
  2. یک یا چند File همراه Metadata/Payload.

Upload یک یا چند File بدون Payload

برای دریافت یک File در Minimal API از IFormFile استفاده می‌شود. File ارسالی از multipart/form-data Bind می‌شود.

Listing 4-29. POST /countries/upload

app.MapPost("/countries/upload", (IFormFile file) =>
    {
        return Results.Created();
    });

FromForm در اینجا ضروری نیست، چون IFormFile ماهیت Source را مشخص می‌کند.

تصویر منبع — صفحهٔ 168Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 168.
شکل ۴-۳۵. Upload فایل countries.csv به POST /countries/upload

نام Key در Form Data باید با نام Parameter یعنی file Match کند؛ Matching به Case حساس نیست. Headerهای مهم عبارت‌اند از Content-Type: multipart/form-data و Content-Length.

تصویر منبع — صفحهٔ 169Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 169.
تصویر منبع — صفحهٔ 169Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 169.
شکل ۴-۳۶. Headerهای ارسال File به Server
شکل ۴-۳۷. محتوای IFormFile در Debug

برای چند File از IFormFileCollection استفاده می‌شود.

تصویر منبع — صفحهٔ 170Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 170.
تصویر منبع — صفحهٔ 170Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 170.

Listing 4-30. POST /countries/uploadmany

app.MapPost("/countries/uploadmany", (IFormFileCollection
    files) =>
    {
        return Results.Created();
    });
شکل ۴-۳۸. Upload چند File به POST /countries/uploadmany

Headerها همان‌اند، ولی مقدارهایی مانند Content-Length با تعداد/حجم Fileها تغییر می‌کنند.

شکل ۴-۳۹. IFormFileCollection هنگام دریافت دو File

هر عضو Collection خود IFormFile است؛ می‌توان روی آن Loop زد و Fileها را به Service فرستاد یا روی Disk ذخیره کرد. نکتهٔ اصلی این بخش نحوهٔ دریافت آن‌ها در Minimal API است.

Upload File همراه Payload

نمی‌توان هم‌زمان JSON Body و Form Data مستقل داشت. وقتی Request با multipart/form-data ارسال می‌شود، Payload نیز باید داخل Form Data قرار گیرد، نه application/json Body. بنابراین Metadata با FromForm Bind می‌شود.

Listing 4-31. Upload یک/چند File همراه Metadata

app.MapPost("/countries/uploadwithmetadata", (
    [FromForm] CountryMetaData countryMetaData, IFormFile file) =>
    {
        return Results.Created();
    }).DisableAntiForgery();

    app.MapPost("/countries/uploadmanywithmetadata", (
    [FromForm] CountryMetaData
    countryMetaData,     IFormFileCollection files) =>
    {
        return Results.Created();
    }).DisableAntiForgery();

هر دو Endpoint رفتار مشابه دارند و Metadata و Fileها از یک Request نوع Form دریافت می‌شوند.

تصویر منبع — صفحهٔ 173Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 173.
تصویر منبع — صفحهٔ 173Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 173.
شکل ۴-۴۰. Upload هم‌زمان File و Metadata
شکل ۴-۴۱. Bind شدن Metadata همراه IFormFileCollection

Validation فایل Uploadشده

برای Security باید مطمئن شویم File تهدیدی ایجاد نمی‌کند. حداقل این موارد بررسی شوند:

  1. Filename فقط شامل Characterهای Alphanumeric و در صورت نیاز Hyphen/Underscore باشد. Slash خطرناک است، چون در Path/Directory معنا دارد.
  2. Extension با نوع مورد انتظار Match کند؛ برای CSV، پسوند .csv.
  3. MIME type درست باشد؛ برای CSV، text/csv.
  4. File Signature یا Magic Bytes: Byteهای ابتدای File نوع واقعی آن را آشکار می‌کنند. مثلاً Executable معمولاً با Hex Sequence برابر 4D 5A یا 5A 4D آغاز می‌شود. صرف Rename کردن Extension نباید Validation را دور بزند.
  5. Content خود File نیز در صورت نیاز Validate شود؛ مثال Regex برای String پیش‌تر دیده شد.
تصویر منبع — صفحهٔ 175Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 175.
شکل ۴-۴۲. Signature یک File اجرایی

برای جلوگیری از Validator عمومی IValidator<IFormFile> که روی همهٔ File Typeها اعمال شود، File و Metadata را در Model مخصوص CountryFileUpload کپسوله می‌کنیم.

Listing 4-32. CountryFileUpload

public class CountryFileUpload
    {
        public IFormFile File { get; set; }

        public string AuthorName { get; set; }
        public string Description { get; set; }
    }

File و Metadata عمداً در یک Level قرار گرفته‌اند تا Complexity کم بماند.

Listing 4-33. CountryFileUploadValidator

using AspNetCore8MinimalApis.Models;
    using FluentValidation;
    using FluentValidation.Results;
    using System.Text.RegularExpressions;

    namespace AspNetCore8MinimalApis.Validators;

    public class CountryFileUploadValidator : AbstractValidator<CountryFileUpload>
    {
        public CountryFileUploadValidator()
        {
            RuleFor(x => x.File).Must((file, context) =>
            {
                return file.File.ContentType == "text/csv";
            }).WithMessage("ContentType is not valid");

            RuleFor(x => x.File).Must((file, context) =>
            {
                return file.File.FileName.EndsWith(".csv");
            }).WithMessage("The file extension is not valid");

            RuleFor(x => x.File.FileName).Matches("^[A-Za-z0-9_\\-.]*$")
            .WithMessage("The file name is not valid");
    
            RuleFor(x => x).Must((file, context) =>
            {
                // string representation of hexadecimal signature of an execute file
                var exeSignatures = new List<string> {
                                      "4D-5A",
                                      "5A 4D"
                                   };
                BinaryReader binary = new BinaryReader(file.File.OpenReadStream());
                byte[] bytes = binary.ReadBytes(2); // reading first two bytes
                string fileSequenceHex = BitConverter.ToString(bytes);
                foreach (var exeSignature in exeSignatures)
                    if (exeSignature.Equals(
                            fileSequenceHex,
                            StringComparison.OrdinalIgnoreCase
                    ))
                        return false;
                return true;
            }).WithName("FileContent")
              .WithMessage("The file content is not valid");

            RuleFor(x => x.AuthorName)
            .NotEmpty()
            .WithMessage("{PropertyName} is required")
            .Custom((authorName, context) =>
            {
               Regex rg = new Regex("<.*?>"); // Matches HTML tags
               if (rg.Matches(authorName).Count > 0)
               {
    
                  // Raises an error
                  context.AddFailure(
                     new ValidationFailure(
                        "AuthorName",
                        "The AuthorName parameter has invalid content"));
               }
            });

            RuleFor(x => x.Description)
            .NotEmpty()
            .WithMessage("{PropertyName} is required")
            .Custom((name, context) =>
            {
                Regex rg = new Regex("<.*?>"); // Matches HTML tags
                if (rg.Matches(name).Count > 0)
                {
                   // Raises an error
                   context.AddFailure(new
                      ValidationFailure(
                         "Name",
                         "The AuthorName parameter has invalid content"));
                }
             });
        }
    }

بخش مهم‌تر Rule مربوط به Magic Bytes است. دو Signature ممنوع EXE به صورت String Hex نگهداری شده‌اند. File با OpenReadStream باز، دو Byte اول با BinaryReader خوانده و با BitConverter.ToString به Hex String تبدیل می‌شود؛ سپس با Signatureهای ممنوع مقایسه می‌گردد.

تصویر منبع — صفحهٔ 179Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 179.
شکل ۴-۴۳. شکست Validation برای File اجرایی که Extension آن به CSV تغییر کرده است

Upload از Download پیچیده‌تر است. Signatureها یا Magic Byteهای انواع File را می‌توان در فهرست‌های استاندارد File Signature بررسی کرد.

Streaming Content

Streaming شبیه Download است، اما Client می‌تواند پیش از کامل‌شدن Download، Content را مصرف کند؛ مثلاً Video را هنگام دریافت پخش کند. برای Stream کردن Video دو چیز لازم است:

  1. Video Stream از نوع Stream.
  2. MIME type مانند video/mp4.

IStreamingService Methodای برای دریافت Stream و MIME type دارد.

Listing 4-34. IStreamingService

namespace Domain.Services;

    public interface IStreamingService
    {
        Task<(Stream stream, string mimeType)> GetFileStream();
    }

Listing 4-35. GET /streaming

app.MapGet("/streaming", async (IStreamingService
    streamingService) =>
    {
        (Stream stream, string mimeType) = await streamingService.
         GetFileStream();
        return Results.Stream(stream, mimeType,
         enableRangeProcessing: true);
    });

Results.Stream با enableRangeProcessing: true به Server اجازه می‌دهد درخواست Range برای قسمت خاصی از Media را مدیریت کند؛ Playerها برای Seek کردن Video از این رفتار استفاده می‌کنند. در چنین حالتی Response می‌تواند Partial Content باشد به‌جای 200 OK.

تصویر منبع — صفحهٔ 182Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 182.

Headerهایی مانند Accept-Ranges، Range و Content-Range که در فصل ۱ دیدیم در همین Context کاربرد دارند.

شکل ۴-۴۴. Streaming یک Video با GET /streaming

Handling CORS

Same-Origin Policy (SOP) به‌صورت پیش‌فرض Scriptهای Browser را محدود می‌کند تا Data را از Origin متفاوت دریافت نکنند. Origin معمولاً Scheme، Host و Port را دربرمی‌گیرد. هدف این Rule جلوگیری از Requestهای ناامن بین Domainهای غیرمجاز است.

برای مشخص‌شدن مجاز بودن Cross-Origin Request، Browser ممکن است Preflight Request با Verb برابر OPTIONS بفرستد. Server با Headerهای Access-Control-* مجاز بودن Origin، Method، Header و Credential را اعلام می‌کند.

  • Access-Control-Allow-Origin: Originهای مجاز.
  • Access-Control-Allow-Credentials: مجاز بودن Requestهای دارای Credential.
  • Access-Control-Allow-Headers: Headerهای مجاز.
  • Access-Control-Allow-Methods: HTTP Methodهای مجاز.
  • Access-Control-Expose-Headers: Headerهایی که Browser اجازه دارد از Response در اختیار Script قرار دهد.
  • Access-Control-Max-Age: زمان Cache شدن Preflight Response به ثانیه.
  • Access-Control-Request-Headers: Headerهایی که Client در Request اصلی قصد ارسال دارد.
  • Access-Control-Request-Method: Method مورد نظر Client برای Request اصلی.

در ASP.NET Core 8 چهار مورد Allow-Origin، Allow-Methods، Allow-Headers و Allow-Credentials رایج‌ترند.

Listing 4-36. Policy آزاد CORS

builder.Services.AddCors(options =>
    {
        options.AddPolicy("AllowAll",
            builder =>
    
    
تصویر منبع — صفحهٔ 185Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 185.
{ builder.AllowAnyHeader() .AllowAnyMethod() .AllowAnyOrigin(); }); });

نمی‌توان هم‌زمان AllowAnyOrigin و AllowCredentials را فعال کرد، زیرا Credential با Origin کاملاً باز ناامن است.

شکل ۴-۴۵. Error ناشی از مجاز کردن Credential و Any Origin هم‌زمان

فقط ثبت Policy کافی نیست؛ Middleware باید در Pipeline فعال شود.

Listing 4-37. فعال‌کردن AllowAll

var app = builder.Build();

    app.UseCors("AllowAll");

برای Production باید Policy محدودتر باشد؛ مثلاً Headerها و Credential مجاز باشند، فقط GET/POST/PUT/DELETE اجازه داشته باشند و Origin تنها دو Domain مشخص باشد.

Listing 4-38. CORS محدودشده

options.AddPolicy("Restricted",
            builder =>
            {
                builder.AllowAnyHeader()
                       .WithMethods("GET", "POST", "PUT", "DELETE")
                       .WithOrigins("https://mydomain.com",
                        "https://myotherdomain.com")
                       .AllowCredentials();
            });

برای آزمون، Script جاوااسکریپت از Origin غیرمجاز http://localhost:5150 تلاش می‌کند API محلی https://localhost:7157 را فراخوانی کند.

Listing 4-39. Request از Origin غیرمجاز

<script>
        function getCountries()
        {
            var xmlHttp = new XMLHttpRequest();
            xmlHttp.onreadystatechange = function() {
                if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
                    callback(xmlHttp.responseText);
            }
            xmlHttp.open("GET", "https://localhost:7157/countries", true);
            xmlHttp.send(null);
        }
        getCountries();
    </script>

چون Origin مجاز نیست، Browser Request را با CORS Error رد می‌کند.

منبع: Coding Clean, Reliable, and Safe REST APIs with ASP.NET Core 8 — Anthony Giretti.

فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.

امتیاز کاربران به این مقاله

☆☆☆☆☆

0 نفر امتیاز داده اند. میانگین: 0.0 از 5

 

0 نظر

نظر محترم شما در مورد مقاله های وب سایت برنامه نویسی و پایگاه داده

نظرات محترم شما در خدمات رسانی بهتر ما را یاری می نمایند. لطفا اگر مایل بودید یک نظر ما را مهمان فرمائید. آدرس ایمیل و وب سایت شما نمایش داده نخواهد شد.

0 / 500

اطلاعات تماس

  • آدرس:اصفهان-خیابان ام کلثوم غربی - بعد خیابان تخم چی - بیست متر بعد از پیتزا ننه شب - کوچه تعمیر گاه سمار زغالی - پلاک 354 - درب مشکی - طبقه هفتم
  • آدرس ایمیل:najafzade@gmail.com
  • وب سایت:http://www.a00b.com/
  • تلفن ثابت:(+98)9131253620
  • تلفن همراه:09131253620