Bagaimana cara membaca tubuh permintaan di pengontrol webapi inti asp.net?


113

Saya mencoba membaca isi permintaan dalam OnActionExecutingmetode, tetapi saya selalu mendapatkan nullisi.

var request = context.HttpContext.Request;
var stream = new StreamReader(request.Body);
var body = stream.ReadToEnd();

Saya telah mencoba untuk secara eksplisit mengatur posisi aliran ke 0, tetapi itu juga tidak berhasil. Karena ini adalah ASP.NET Core, menurut saya sedikit berbeda. Saya dapat melihat semua contoh di sini mengacu pada versi API web lama.

Apakah ada cara lain untuk melakukan ini?


5
Hati-hati, jika isi request sudah dibaca sebelumnya selama request pipeline, maka kosong saat Anda mencoba membacanya untuk kedua kalinya
Fabio


@Fabio Terima kasih atas infonya, bisakah kita atur posisi dan membacanya lagi?
Kasun Koswattha

@KasunKoswattha - Secara desain, konten tubuh diperlakukan sebagai aliran hanya-maju yang dapat dibaca hanya sekali.
pengguna270576

Saya kira pertanyaannya lebih menargetkan filter atau middleware daripada pengontrol.
Jim Aho

Jawaban:


115

Di ASP.Net Core tampaknya rumit untuk membaca beberapa kali permintaan tubuh, namun jika upaya pertama Anda melakukannya dengan cara yang benar, Anda akan baik-baik saja untuk upaya berikutnya.

Saya membaca beberapa perubahan haluan misalnya dengan mengganti aliran tubuh, tetapi saya pikir berikut ini yang paling bersih:

Poin terpenting adalah

  1. untuk memberi tahu permintaan bahwa Anda akan membaca tubuhnya dua kali atau lebih,
  2. untuk tidak menutup aliran tubuh, dan
  3. untuk memundurkannya ke posisi awal sehingga proses internal tidak hilang.

[EDIT]

Seperti yang ditunjukkan oleh Murad, Anda juga dapat memanfaatkan ekstensi .Net Core 2.1: EnableBufferingIni menyimpan permintaan besar ke disk alih-alih menyimpannya di memori, menghindari masalah aliran besar yang disimpan dalam memori (file, gambar, ...) . Anda dapat mengubah folder sementara dengan mengatur ASPNETCORE_TEMPvariabel lingkungan, dan file akan dihapus setelah permintaan selesai.

Di AuthorizationFilter , Anda bisa melakukan hal berikut:

// Helper to enable request stream rewinds
using Microsoft.AspNetCore.Http.Internal;
[...]
public class EnableBodyRewind : Attribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var bodyStr = "";
        var req = context.HttpContext.Request;

        // Allows using several time the stream in ASP.Net Core
        req.EnableRewind(); 

        // Arguments: Stream, Encoding, detect encoding, buffer size 
        // AND, the most important: keep stream opened
        using (StreamReader reader 
                  = new StreamReader(req.Body, Encoding.UTF8, true, 1024, true))
        {
            bodyStr = reader.ReadToEnd();
        }

        // Rewind, so the core is not lost when it looks the body for the request
        req.Body.Position = 0;

        // Do whatever work with bodyStr here

    }
}



public class SomeController : Controller
{
    [HttpPost("MyRoute")]
    [EnableBodyRewind]
    public IActionResult SomeAction([FromBody]MyPostModel model )
    {
        // play the body string again
    }
}

Kemudian Anda dapat menggunakan isi kembali di penangan permintaan.

Dalam kasus Anda, jika Anda mendapatkan hasil nol, itu mungkin berarti bahwa isi telah dibaca pada tahap sebelumnya. Dalam hal ini Anda mungkin perlu menggunakan middleware (lihat di bawah).

Namun berhati-hatilah jika Anda menangani aliran besar, perilaku tersebut menyiratkan bahwa semuanya dimuat ke dalam memori, ini tidak boleh dipicu jika ada file yang diunggah.

Anda mungkin ingin menggunakan ini sebagai Middleware

Milik saya terlihat seperti ini (sekali lagi, jika Anda mengunduh / mengunggah file besar, ini harus dinonaktifkan untuk menghindari masalah memori):

public sealed class BodyRewindMiddleware
{
    private readonly RequestDelegate _next;

    public BodyRewindMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try { context.Request.EnableRewind(); } catch { }
        await _next(context);
        // context.Request.Body.Dipose() might be added to release memory, not tested
    }
}
public static class BodyRewindExtensions
{
    public static IApplicationBuilder EnableRequestBodyRewind(this IApplicationBuilder app)
    {
        if (app == null)
        {
            throw new ArgumentNullException(nameof(app));
        }

        return app.UseMiddleware<BodyRewindMiddleware>();
    }

}

aliran masih kosong bahkan jika saya mundur ke Posisi 0.
Adrian Nasui

2
Apakah Anda menggunakan req.EnableRewind();? Saya menggunakan kode di atas dan berfungsi dengan baik.
Jean

1
telah menggunakan req.EnableRewind (); tidak bekerja. Saya mendapatkan Posisi = 0, panjang tubuh = 26, tetapi membaca aliran 'tubuh' muncul dengan string kosong.
Adrian Nasui

Ini adalah jawaban yang sempurna
Gayan

3
Dimungkinkan juga untuk menggunakan request.EnableBuffering()(pembungkus di atas EnableRewind()) Ini tersedia di ASP.NET Core 2.1 docs.microsoft.com/en-us/dotnet/api/…
Murad

29

Solusi yang lebih jelas, bekerja di ASP.Net Core 2.1 / 3.1

Kelas filter

using Microsoft.AspNetCore.Authorization;
// For ASP.NET 2.1
using Microsoft.AspNetCore.Http.Internal;
// For ASP.NET 3.1
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;

public class ReadableBodyStreamAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        // For ASP.NET 2.1
        // context.HttpContext.Request.EnableRewind();
        // For ASP.NET 3.1
        // context.HttpContext.Request.EnableBuffering();
    }
}

Dalam sebuah Kontroler

[HttpPost]
[ReadableBodyStream]
public string SomePostMethod()
{
    //Note: if you're late and body has already been read, you may need this next line
    //Note2: if "Note" is true and Body was read using StreamReader too, then it may be necessary to set "leaveOpen: true" for that stream.
    HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);

    using (StreamReader stream = new StreamReader(HttpContext.Request.Body))
    {
        string body = stream.ReadToEnd();
        // body = "param=somevalue&param2=someothervalue"
    }
}

2
Untuk netcore3.0, itu akan menjadi .EnableBuffering () daripada.EnableRewind()
mr5

Terima kasih @ mr5 - Memperbarui jawaban saya
Andriod

Saya menemukan ini saat memperbaiki beberapa upgrade .net Core 2.2 -> Core 3.1 yang merusak cara EnableRewind (). Saya rasa ini membutuhkan satu baris kode lagi, tanpanya saya tidak dapat membaca kembali Body: HttpContext.Request.Body.Seek (0, SeekOrigin.Begin);
Larry Smith

2
Ini hanya berfungsi untuk saya setelah mengubah AuthorizeAttributeke Attribute(di ASP.Net Core 3.1).
Sigmund

Guys tolong pastikan untuk menambahkan perpustakaan yang disebutkan. Saya sudah memiliki kode tetapi EnableBuffering menunjukkan garis berlekuk-lekuk merah sampai saya menyadari referensi Microsoft.AspNetCore.Http hilang. Berkat Android!
kaarthick raman

18

Untuk dapat memundurkan tubuh permintaan, jawaban @Jean membantu saya menemukan solusi yang tampaknya berfungsi dengan baik. Saat ini saya menggunakan ini untuk Global Exception Handler Middleware tetapi prinsipnya sama.

Saya membuat middleware yang pada dasarnya memungkinkan pemunduran pada tubuh permintaan (bukan dekorator).

using Microsoft.AspNetCore.Http.Internal;
[...]
public class EnableRequestRewindMiddleware
{
    private readonly RequestDelegate _next;

    public EnableRequestRewindMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        context.Request.EnableRewind();
        await _next(context);
    }
}

public static class EnableRequestRewindExtension
{
    public static IApplicationBuilder UseEnableRequestRewind(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<EnableRequestRewindMiddleware>();
    }
}

Ini kemudian dapat digunakan di Startup.cslike Anda :

[...]
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    [...]
    app.UseEnableRequestRewind();
    [...]
}

Dengan menggunakan pendekatan ini, saya telah berhasil memundurkan aliran tubuh permintaan.


1
Ini bekerja dengan baik untuk saya @SaoBiz terima kasih! satu kesalahan ketik, perubahan 2 ini untuk pembangun di UseEnableRequestRewind(this IApplicationBuilder builder).
Richard Logwood

@RichardLogwood Senang bisa membantu! Terima kasih telah menemukan kesalahan ketik! Tetap. :)
SaoBiz

7

Ini adalah utas lama, tetapi sejak saya tiba di sini, saya pikir saya akan memposting temuan saya sehingga mereka dapat membantu orang lain.

Pertama, saya memiliki masalah yang sama, di mana saya ingin mendapatkan Request.Body dan melakukan sesuatu dengan itu (logging / audit). Tetapi sebaliknya saya ingin titik akhir terlihat sama.

Jadi, sepertinya panggilan EnableBuffering () mungkin berhasil. Kemudian Anda dapat melakukan Seek (0, xxx) pada tubuh dan membaca kembali isinya, dll.

Namun, hal ini menyebabkan masalah saya berikutnya. Saya akan mendapatkan pengecualian "Operasi sinkronis tidak diizinkan" saat mengakses titik akhir. Jadi, solusinya adalah mengatur properti AllowSynchronousIO = true, dalam opsi. Ada beberapa cara untuk melakukannya (tetapi tidak penting untuk detailnya di sini ..)

LALU, masalah berikutnya adalah ketika saya pergi untuk membaca Request. Tubuh itu sudah dibuang. Ugh. Jadi, apa yang menyebabkannya?

Saya menggunakan Newtonsoft.JSON sebagai parser [FromBody] saya di panggilan endpiont. Itulah yang bertanggung jawab atas pembacaan sinkron dan juga menutup aliran saat selesai. Larutan? Baca aliran sebelum masuk ke penguraian JSON? Tentu, itu berhasil dan saya berakhir dengan ini:

 /// <summary>
/// quick and dirty middleware that enables buffering the request body
/// </summary>
/// <remarks>
/// this allows us to re-read the request body's inputstream so that we can capture the original request as is
/// </remarks>
public class ReadRequestBodyIntoItemsAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (context == null) return;

        // NEW! enable sync IO beacuse the JSON reader apparently doesn't use async and it throws an exception otherwise
        var syncIOFeature = context.HttpContext.Features.Get<IHttpBodyControlFeature>();
        if (syncIOFeature != null)
        {
            syncIOFeature.AllowSynchronousIO = true;

            var req = context.HttpContext.Request;

            req.EnableBuffering();

            // read the body here as a workarond for the JSON parser disposing the stream
            if (req.Body.CanSeek)
            {
                req.Body.Seek(0, SeekOrigin.Begin);

                // if body (stream) can seek, we can read the body to a string for logging purposes
                using (var reader = new StreamReader(
                     req.Body,
                     encoding: Encoding.UTF8,
                     detectEncodingFromByteOrderMarks: false,
                     bufferSize: 8192,
                     leaveOpen: true))
                {
                    var jsonString = reader.ReadToEnd();

                    // store into the HTTP context Items["request_body"]
                    context.HttpContext.Items.Add("request_body", jsonString);
                }

                // go back to beginning so json reader get's the whole thing
                req.Body.Seek(0, SeekOrigin.Begin);
            }
        }
    }
}

Jadi sekarang, saya dapat mengakses tubuh menggunakan HttpContext.Items ["request_body"] di titik akhir yang memiliki atribut [ReadRequestBodyIntoItems].

Tapi kawan, ini sepertinya terlalu banyak rintangan untuk dilewati. Jadi di sinilah saya mengakhiri, dan saya sangat senang dengannya.

Titik akhir saya dimulai sebagai sesuatu seperti:

[HttpPost("")]
[ReadRequestBodyIntoItems]
[Consumes("application/json")]
public async Task<IActionResult> ReceiveSomeData([FromBody] MyJsonObjectType value)
{
    val bodyString = HttpContext.Items["request_body"];
    // use the body, process the stuff...
}

Tetapi jauh lebih mudah untuk hanya mengubah tanda tangan, seperti:

[HttpPost("")]
[Consumes("application/json")]
public async Task<IActionResult> ReceiveSomeData()
{
    using (var reader = new StreamReader(
           Request.Body,
           encoding: Encoding.UTF8,
           detectEncodingFromByteOrderMarks: false
    ))
    {
        var bodyString = await reader.ReadToEndAsync();

        var value = JsonConvert.DeserializeObject<MyJsonObjectType>(bodyString);

        // use the body, process the stuff...
    }
}

Saya sangat menyukai ini karena hanya membaca aliran tubuh sekali, dan saya memiliki kendali atas deserialisasi. Tentu, bagus jika inti ASP.NET melakukan keajaiban ini untuk saya, tetapi di sini saya tidak membuang waktu membaca aliran dua kali (mungkin buffering setiap kali), dan kodenya cukup jelas dan bersih.

Jika Anda memerlukan fungsionalitas ini pada banyak titik akhir, mungkin pendekatan middleware mungkin lebih bersih, atau setidaknya Anda dapat merangkum ekstraksi tubuh ke dalam fungsi ekstensi untuk membuat kode lebih ringkas.

Bagaimanapun, saya tidak menemukan sumber apa pun yang menyentuh ketiga aspek masalah ini, karenanya posting ini. Semoga ini membantu seseorang!

BTW: Ini menggunakan ASP .NET Core 3.1.


Jika program tidak dapat mengurai string JSON menjadi NyObjectType, maka saya tidak dapat membaca nilai dari "request_body"
Ericyu67

3

Baru-baru ini saya menemukan solusi yang sangat elegan yang mengambil JSON acak sehingga Anda tidak tahu strukturnya:

    [HttpPost]
    public JsonResult Test([FromBody] JsonElement json)
    {
        return Json(json);
    }

Semudah itu.


Terima kasih, ini benar-benar berhasil. Saya menggunakan GetRawText()metode JsonElement dan menerima teks JSON saya.
mrNone

Ini tidak akan memberi Anda badan permintaan yang sebenarnya jika DTO melakukan beberapa pemrosesan saat Anda membuatnya, seperti menyetel nilai default atau semacamnya.
Ebram Shehata

3

Cara cepat untuk menambahkan buffering respons di .NET Core 3.1 adalah

    app.Use((context, next) =>
    {
        context.Request.EnableBuffering();
        return next();
    });

di Startup.cs. Saya menemukan ini juga menjamin bahwa buffering akan diaktifkan sebelum streaming dibaca, yang merupakan masalah untuk .Net Core 3.1 dengan beberapa jawaban filter middleware / otorisasi lain yang pernah saya lihat.

Kemudian Anda dapat membaca isi permintaan Anda melalui HttpContext.Request.Bodydi handler Anda seperti yang disarankan beberapa orang lain.

Juga patut dipertimbangkan adalah yang EnableBufferingmemiliki kelebihan yang memungkinkan Anda untuk membatasi berapa banyak buffer dalam memori sebelum menggunakan file sementara, dan juga batas keseluruhan untuk Anda buffer. NB jika permintaan melebihi batas ini pengecualian akan dilempar dan permintaan tidak akan pernah mencapai penangan Anda.


Ini bekerja dengan sangat baik bagi saya (3.1). Mengutip Anda pada pertanyaan lain: stackoverflow.com/a/63525694/6210068
Lance

Dikerjakan 3.1. Juga, sebagai pengingat bagi pengguna yang akan menggunakan ini: Pastikan untuk menempatkannya dalam urutan yang benar di Startup.cs.
Ebram Shehata

2

Saya mengalami masalah serupa saat menggunakan ASP.NET Core 2.1:

  • Saya memerlukan middleware khusus untuk membaca data yang telah di-POSTING dan melakukan beberapa pemeriksaan keamanan terhadapnya
  • menggunakan filter otorisasi tidak praktis, karena banyaknya tindakan yang terpengaruh
  • Saya harus mengizinkan objek mengikat dalam tindakan ([FromBody] someObject). Terima kasih SaoBizuntuk menunjukkan solusi ini.

Jadi, solusi yang jelas adalah mengizinkan permintaan untuk dapat diputar ulang, tetapi pastikan bahwa setelah membaca isi, pengikatan masih berfungsi.

EnableRequestRewindMiddleware

public class EnableRequestRewindMiddleware
{
    private readonly RequestDelegate _next;

    ///<inheritdoc/>
    public EnableRequestRewindMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task Invoke(HttpContext context)
    {
        context.Request.EnableRewind();
        await _next(context);
    }
}

Startup.cs

(tempatkan ini di awal metode Konfigurasi)

app.UseMiddleware<EnableRequestRewindMiddleware>();

Beberapa middleware lainnya

Ini adalah bagian dari middleware yang membutuhkan pembongkaran informasi POST untuk memeriksa sesuatu.

using (var stream = new MemoryStream())
{
    // make sure that body is read from the beginning
    context.Request.Body.Seek(0, SeekOrigin.Begin);
    context.Request.Body.CopyTo(stream);
    string requestBody = Encoding.UTF8.GetString(stream.ToArray());

    // this is required, otherwise model binding will return null
    context.Request.Body.Seek(0, SeekOrigin.Begin);
}

2

untuk membaca Body, Anda dapat membaca secara asinkron.

gunakan asyncmetode seperti berikut:

public async Task<IActionResult> GetBody()
{
      string body="";
      using (StreamReader stream = new StreamReader(Request.Body))
      {
           body = await stream.ReadToEndAsync();
      }
    return Json(body);
}

Uji dengan tukang pos:

masukkan deskripsi gambar di sini

Ini bekerja dengan baik dan diuji dalam Asp.net coreversi 2.0 , 2.1 , 2.2, 3.0.

Semoga bermanfaat


1

The IHttpContextAccessormetode tidak bekerja jika Anda ingin pergi rute ini.

TLDR;

  • Injeksi IHttpContextAccessor

  • Mundur - HttpContextAccessor.HttpContext.Request.Body.Seek(0, System.IO.SeekOrigin.Begin);

  • Baca -- System.IO.StreamReader sr = new System.IO.StreamReader(HttpContextAccessor.HttpContext.Request.Body); JObject asObj = JObject.Parse(sr.ReadToEnd());

More - Upaya pada contoh item yang ringkas dan tidak dapat dikompilasi, yang perlu Anda pastikan ada untuk mendapatkan yang bisa digunakan IHttpContextAccessor. Jawaban telah menunjukkan dengan benar bahwa Anda harus mencari kembali ke awal ketika Anda mencoba membaca isi permintaan. Itu CanSeek, Positionproperti pada aliran tubuh permintaan membantu untuk memverifikasi ini.

.NET Core DI Docs

// First -- Make the accessor DI available
//
// Add an IHttpContextAccessor to your ConfigureServices method, found by default
// in your Startup.cs file:
// Extraneous junk removed for some brevity:
public void ConfigureServices(IServiceCollection services)
{
    // Typical items found in ConfigureServices:
    services.AddMvc(config => { config.Filters.Add(typeof(ExceptionFilterAttribute)); });
    // ...

    // Add or ensure that an IHttpContextAccessor is available within your Dependency Injection container
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}

// Second -- Inject the accessor
//
// Elsewhere in the constructor of a class in which you want
// to access the incoming Http request, typically 
// in a controller class of yours:
public class MyResourceController : Controller
{
    public ILogger<PricesController> Logger { get; }
    public IHttpContextAccessor HttpContextAccessor { get; }

    public CommandController(
        ILogger<CommandController> logger,
        IHttpContextAccessor httpContextAccessor)
    {
        Logger = logger;
        HttpContextAccessor = httpContextAccessor;
    }

    // ...

    // Lastly -- a typical use 
    [Route("command/resource-a/{id}")]
    [HttpPut]
    public ObjectResult PutUpdate([FromRoute] string id, [FromBody] ModelObject requestModel)
    {
        if (HttpContextAccessor.HttpContext.Request.Body.CanSeek)
        {
            HttpContextAccessor.HttpContext.Request.Body.Seek(0, System.IO.SeekOrigin.Begin);
            System.IO.StreamReader sr = new System.IO.StreamReader(HttpContextAccessor.HttpContext.Request.Body);
            JObject asObj = JObject.Parse(sr.ReadToEnd());

            var keyVal = asObj.ContainsKey("key-a");
        }
    }
}    

1

Saya dapat membaca tubuh permintaan dalam aplikasi asp.net core 3.1 seperti ini (bersama dengan middleware sederhana yang memungkinkan buffering -mengaktifkan rewinding tampaknya berfungsi untuk versi .Net Core sebelumnya-):

var reader = await Request.BodyReader.ReadAsync();
Request.Body.Position = 0;
var buffer = reader.Buffer;
var body = Encoding.UTF8.GetString(buffer.FirstSpan);
Request.Body.Position = 0;

0

Saya juga ingin membaca Request.Body tanpa secara otomatis memetakannya ke beberapa model parameter tindakan. Menguji banyak cara berbeda sebelum memecahkan ini. Dan saya tidak menemukan solusi yang berfungsi seperti yang dijelaskan di sini. Solusi ini saat ini didasarkan pada kerangka kerja .NET Core 3.0.

reader.readToEnd () seamed seperti cara sederhana, meskipun dikompilasi, itu melontarkan pengecualian waktu proses yang mengharuskan saya untuk menggunakan panggilan async. Jadi, saya menggunakan ReadToEndAsync (), namun terkadang berhasil, dan terkadang tidak. Memberi saya kesalahan seperti, tidak dapat membaca setelah streaming ditutup. Masalahnya adalah kami tidak dapat menjamin bahwa itu akan mengembalikan hasil di utas yang sama (bahkan jika kami menggunakan await). Jadi kami membutuhkan semacam panggilan balik. Solusi ini berhasil untuk saya.

[Route("[controller]/[action]")]
public class MyController : ControllerBase
{

    // ...

    [HttpPost]
    public async void TheAction()
    {
        try
        {
            HttpContext.Request.EnableBuffering();
            Request.Body.Position = 0;
            using (StreamReader stream = new StreamReader(HttpContext.Request.Body))
            {
                var task = stream
                    .ReadToEndAsync()
                    .ContinueWith(t => {
                        var res = t.Result;
                        // TODO: Handle the post result!
                    });

                // await processing of the result
                task.Wait();
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to handle post!");
        }
    }

0

Cara yang paling sederhana untuk melakukannya adalah sebagai berikut:

  1. Dalam metode Controller Anda perlu mengekstrak tubuh dari, tambahkan parameter ini: Nilai SomeClass [FromBody]

  2. Deklarasikan "SomeClass" sebagai: class SomeClass {public string SomeParameter {get; set; }}

Ketika tubuh mentah dikirim sebagai json, inti .net tahu cara membacanya dengan sangat mudah.


0

Untuk mereka yang hanya ingin mendapatkan konten (request body) dari request:

Gunakan [FromBody]atribut dalam parameter metode pengontrol Anda.

[Route("api/mytest")]
[ApiController]
public class MyTestController : Controller
{
    [HttpPost]
    [Route("content")]
    public async Task<string> ReceiveContent([FromBody] string content)
    {
        // Do work with content
    }
}

Seperti yang dikatakan doc: atribut ini menetapkan bahwa parameter atau properti harus diikat menggunakan isi permintaan.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.