İstemci tarayıcısından Amazon S3 doğrudan dosya yükleme - özel anahtar açıklaması


159

Herhangi bir sunucu tarafı kodu olmadan, sadece JavaScript kullanarak REST API üzerinden istemci makineden Amazon S3 doğrudan dosya yükleme uyguluyorum. Her şey yolunda gidiyor ama bir şey beni endişelendiriyor ...

Amazon S3 REST API'ye bir istek gönderdiğimde, isteği imzalamam ve Authenticationüstbilgiye imza koymam gerekiyor . İmza oluşturmak için gizli anahtarımı kullanmalıyım. Ancak her şey istemci tarafında gerçekleşir, bu nedenle gizli anahtar sayfa kaynağından kolayca ortaya çıkarılabilir (kaynaklarımı gizlememe / şifreleme bile).

Bunu nasıl halledebilirim? Ve bu hiç sorun değil mi? Belirli özel anahtar kullanımını yalnızca belirli bir CORS Origin'ten REST API çağrılarıyla ve yalnızca PUT ve POST yöntemleriyle veya belki de anahtarı yalnızca S3 ve belirli bir kova ile sınırlandırabilirim? Başka bir kimlik doğrulama yöntemi olabilir mi?

"Sunucusuz" çözüm idealdir, ancak sunucuma bir dosya yüklemek ve daha sonra S3'e göndermek dışında bazı sunucu tarafı işlemlerini de dahil etmeyi düşünebilirim.


7
Çok basit: sırları istemci tarafında saklamayın. İsteği imzalamak için bir sunucu eklemeniz gerekir.
Ray Nicholus

1
Ayrıca, bu istekleri kodlamanın imzalanması ve base-64'ün sunucu tarafında çok daha kolay olduğunu göreceksiniz. Buraya bir sunucu dahil etmek mantıksız görünmüyor. Tüm dosya baytlarını bir sunucuya ve daha sonra S3'e göndermek istemediğimi anlayabiliyorum, ancak istekleri istemci tarafına imzalamanın çok az yararı var, çünkü bu özellikle istemci tarafı yapmak biraz zor ve potansiyel olarak yavaş olacak (javascript'te).
Ray Nicholus

5
2016, sunucusuz mimari oldukça popüler hale geldiğinden, dosyaları doğrudan S3'e yüklemek AWS Lambda'nın yardımıyla mümkündür. Benzer bir soruya verdiğim cevaba bakın: stackoverflow.com/a/40828683/2504317 Temelde her dosya için karşıya yüklenebilen URL imzalayan bir API olarak Lambda işleviniz olacak ve cliend tarafındaki javascript'iniz önceden imzalanmış URL. Böyle şeyler yapan bir Vue bileşeni yazdım, S3 yükleme ile ilgili kod kütüphane agnostik, bir göz atın ve fikir olsun.
KF Lin

Herhangi bir S3 grubuna HTTP / S POST yüklemesi için başka bir 3. taraf. JS3Upload saf HTML5: jfileupload.com/products/js3upload-html5/index.html
JFU

Yanıtlar:


216

İstediğinizi POST Kullanarak Tarayıcı Tabanlı Yüklemeler olduğunu düşünüyorum.

Temel olarak, sunucu tarafı koda ihtiyacınız vardır, ancak tek yaptığı imzalı ilkeler oluşturmaktır. İstemci tarafı kodu imzalanan ilkeye sahip olduktan sonra, veriler sunucunuzdan geçmeden POST kullanarak doğrudan S3'e yüklenebilir.

İşte resmi doc bağlantıları:

Diyagram: http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingHTTPPOST.html

Örnek kod: http://docs.aws.amazon.com/AmazonS3/latest/dev/HTTPPOSTExamples.html

İmzalı politika HTML'nize şu şekilde gönderilir:

<html>
  <head>
    ...
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    ...
  </head>
  <body>
  ...
  <form action="http://johnsmith.s3.amazonaws.com/" method="post" enctype="multipart/form-data">
    Key to upload: <input type="input" name="key" value="user/eric/" /><br />
    <input type="hidden" name="acl" value="public-read" />
    <input type="hidden" name="success_action_redirect" value="http://johnsmith.s3.amazonaws.com/successful_upload.html" />
    Content-Type: <input type="input" name="Content-Type" value="image/jpeg" /><br />
    <input type="hidden" name="x-amz-meta-uuid" value="14365123651274" />
    Tags for File: <input type="input" name="x-amz-meta-tag" value="" /><br />
    <input type="hidden" name="AWSAccessKeyId" value="AKIAIOSFODNN7EXAMPLE" />
    <input type="hidden" name="Policy" value="POLICY" />
    <input type="hidden" name="Signature" value="SIGNATURE" />
    File: <input type="file" name="file" /> <br />
    <!-- The elements after this will be ignored -->
    <input type="submit" name="submit" value="Upload to Amazon S3" />
  </form>
  ...
</html>

FORM eyleminin dosyayı sunucunuz aracılığıyla değil, doğrudan S3'e gönderdiğine dikkat edin .

Kullanıcılarınızdan biri her dosya yüklemek istediğinde , sunucunuzda POLICYve SIGNATUREsunucunuzda oluşturulur. Sayfayı kullanıcının tarayıcısına döndürürsünüz. Kullanıcı daha sonra sunucunuzdan geçmeden doğrudan S3'e bir dosya yükleyebilir.

Politikayı imzaladığınızda, genellikle politikanın süresinin birkaç dakika sonra dolmasını sağlarsınız. Bu, yüklemeden önce kullanıcılarınızı sunucunuzla konuşmaya zorlar. Bu, isterseniz yüklemeleri izlemenizi ve sınırlamanızı sağlar.

Sunucunuza giden veya sunucunuzdan tek veri imzalı URL'lerdir. Gizli anahtarlarınız sunucuda gizli kalır.


14
lütfen bunun yakında v4 ile değiştirilecek İmza v2'yi kullandığını unutmayın: docs.aws.amazon.com/AmazonS3/latest/API/…
Jörn Berkefeld

9
${filename}Anahtar adına eklediğinizden emin olun , bu nedenle yukarıdaki örnek için user/eric/${filename}yalnızca yerine user/eric. Eğer user/ericmevcut bir klasör, yükleme sessizce başarısız olur (hatta success_action_redirect yönlendirilecektir) ve yüklenen içerik kalmayacaktır. Bu bir hata sorunu olduğunu düşünerek hata ayıklamak için saatler geçirdim.
Balint Erdi

@secretmike Bu yöntemi yaparken bir zaman aşımı aldıysanız, bunu dolaşmak için nasıl bir öneri sunabilirsiniz?
Yolculuk

1
@Trip Tarayıcı dosyayı S3'e gönderdiğinden, Javascript'teki zaman aşımını algılamanız ve yeniden denemenizi başlatmanız gerekir.
16'da

@secretmike Sonsuz bir döngü döngüsü gibi kokuyor. Zaman aşımı süresinin 10 / mbs üzerindeki dosyalar için süresiz olarak yineleneceği için.
Yolculuk

40

Bunu AWS S3 Cognito ile yapabilirsiniz, bu bağlantıyı burada deneyin:

http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/browser-examples.html#Amazon_S3

Ayrıca bu kodu deneyin

Bölge, IdentityPoolId ve grup adınızı değiştirmeniz yeterlidir

<!DOCTYPE html>
<html>

<head>
    <title>AWS S3 File Upload</title>
    <script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js"></script>
</head>

<body>
    <input type="file" id="file-chooser" />
    <button id="upload-button">Upload to S3</button>
    <div id="results"></div>
    <script type="text/javascript">
    AWS.config.region = 'your-region'; // 1. Enter your region

    AWS.config.credentials = new AWS.CognitoIdentityCredentials({
        IdentityPoolId: 'your-IdentityPoolId' // 2. Enter your identity pool
    });

    AWS.config.credentials.get(function(err) {
        if (err) alert(err);
        console.log(AWS.config.credentials);
    });

    var bucketName = 'your-bucket'; // Enter your bucket name
    var bucket = new AWS.S3({
        params: {
            Bucket: bucketName
        }
    });

    var fileChooser = document.getElementById('file-chooser');
    var button = document.getElementById('upload-button');
    var results = document.getElementById('results');
    button.addEventListener('click', function() {

        var file = fileChooser.files[0];

        if (file) {

            results.innerHTML = '';
            var objKey = 'testing/' + file.name;
            var params = {
                Key: objKey,
                ContentType: file.type,
                Body: file,
                ACL: 'public-read'
            };

            bucket.putObject(params, function(err, data) {
                if (err) {
                    results.innerHTML = 'ERROR: ' + err;
                } else {
                    listObjs();
                }
            });
        } else {
            results.innerHTML = 'Nothing to upload.';
        }
    }, false);
    function listObjs() {
        var prefix = 'testing';
        bucket.listObjects({
            Prefix: prefix
        }, function(err, data) {
            if (err) {
                results.innerHTML = 'ERROR: ' + err;
            } else {
                var objKeys = "";
                data.Contents.forEach(function(obj) {
                    objKeys += obj.Key + "<br>";
                });
                results.innerHTML = objKeys;
            }
        });
    }
    </script>
</body>

</html>

Daha fazla bilgi için lütfen kontrol edin - Github

Bu birden fazla görüntüyü destekliyor mu?
user2722667

@ user2722667 evet öyle.
Joomler

@Joomler Merhaba Teşekkürler ama firefox RequestTimeout bu sorunla karşı karşıya Sunucunuza soket bağlantısı okunmamış veya zaman aşımı süresi içinde yazılı değildi. Boşta bağlantıları kapatıldı ve dosya i bu issue.Thanks düzeltebilirim nasıl beni yardım lütfen S3.Can ağda yükle gelmez edilecektir
usama

1
@ usama sorunu github'da açabilir misiniz, çünkü sorun bana açık değil
Joomler

@Joomler geç cevap için üzgünüm burada GitHub bir sorun açtı lütfen bu bir göz atın Teşekkürler. github.com/aws/aws-sdk-php/issues/1332
usama

16

"Sunucusuz" bir çözüm istediğinizi söylüyorsunuz. Ancak bu, herhangi bir "kod" kodunuzu döngüye koyamayacağınız anlamına gelir. (NOT: Kodunuzu bir istemciye verdiğinizde, bu kod artık "kodlarıdır." sisteminizi kötüye kullanmak için doğru CORS başlığını kullanın.

En büyük sorun, farklı kullanıcılar arasında ayrım yapamamanızdır. Bir kullanıcının dosyalarını listelemesine / dosyalarına erişmesine izin veremezsiniz, ancak başkalarının bunu yapmasını engelleyemezsiniz. Kötüye kullanımı tespit ederseniz, anahtarı değiştirmek dışında yapabileceğiniz hiçbir şey yoktur. (Saldırganın muhtemelen tekrar alabileceği.)

En iyi seçeneğiniz, javascript istemciniz için bir anahtar içeren bir "IAM kullanıcısı" oluşturmaktır. Sadece bir kovaya yazma erişimi verin. (ancak ideal olarak, saldırganlar için daha çekici hale getirecek ListBucket işlemini etkinleştirmeyin.)

Bir sunucunuz varsa (ayda 20 ABD doları değerinde basit bir mikro bulut sunucusu bile), gerçek zamanlı olarak kötüye kullanımı izlerken / önlerken sunucunuzdaki anahtarları imzalayabilirsiniz. Bir sunucu olmadan, yapabileceğiniz en iyi şey, olaydan sonra kötüye kullanım için düzenli olarak izlemektir. İşte yapacağım şey:

1) o IAM kullanıcısı için anahtarları periyodik olarak döndürün: Her gece o IAM kullanıcısı için yeni bir anahtar oluşturun ve en eski anahtarı değiştirin. 2 anahtar olduğundan, her anahtar 2 gün boyunca geçerli olacaktır.

2) S3 günlük kaydını etkinleştirin ve günlükleri her saat indirin. "Çok fazla yükleme" ve "çok fazla indirme" konusunda uyarı ayarlayın. Hem toplam dosya boyutunu hem de yüklenen dosya sayısını kontrol etmek isteyeceksiniz. Hem genel toplamları hem de IP başına toplamları (daha düşük bir eşikle) izlemek istersiniz.

Bu kontroller "sunucusuz" olarak yapılabilir, çünkü bunları masaüstünüzde çalıştırabilirsiniz. (yani S3 tüm işi yapar, bu işlemler S3 kepçenizi kötüye kullanmanız konusunda sizi uyarır, böylece ay sonunda dev bir AWS faturası almazsınız .)


3
Dostum, Lambda'dan önce işlerin ne kadar karmaşık olduğunu unuttum.
Ryan Shillington

10

Kabul edilen yanıta daha fazla bilgi ekleyerek, AWS İmza sürüm 4'ü kullanarak kodun çalışan bir sürümünü görmek için bloguma başvurabilirsiniz.

Burada özetleyeceğiz:

Kullanıcı yüklenecek bir dosyayı seçer seçmez aşağıdakileri yapın: 1. Gerekli parametreleri oluşturmak için bir hizmet başlatmak üzere web sunucusunu arayın

  1. Bu serviste, geçici kredi almak için AWS IAM servisini arayın

  2. Krediye sahip olduğunuzda, bir grup ilkesi oluşturun (temel 64 kodlu dize). Ardından son imzayı oluşturmak için grup politikasını geçici gizli erişim anahtarıyla imzalayın

  3. gerekli parametreleri kullanıcı arayüzüne geri gönder

  4. Bu alındıktan sonra, bir html form nesnesi oluşturun, gerekli parametreleri ayarlayın ve POST yapın.

Ayrıntılı bilgi için lütfen https://wordpress1763.wordpress.com/2016/10/03/browser-based-upload-aws-signature-version-4/


5
Javascript'te bunu anlamaya çalışırken bir gün geçirdim ve bu cevap bana XMLhttprequest kullanarak bunu nasıl yapacağımı tam olarak anlatıyor. İndirildiğinize çok şaşırdım. OP javascript istedi ve önerilen cevaplarda formlar aldı. İyi keder. Bu cevap için teşekkürler!
Paul S

BTW superagent'ın ciddi CORS sorunları var, bu nedenle xmlhttprequest şu anda bunu yapmanın tek makul yolu gibi görünüyor
Paul S

4

İmza oluşturmak için gizli anahtarımı kullanmalıyım. Ancak her şey istemci tarafında gerçekleşir, bu nedenle gizli anahtar sayfa kaynağından kolayca ortaya çıkarılabilir (kaynaklarımı gizlememe / şifreleme bile).

Yanlış anladığınız yer burası. Dijital imzaların kullanılmasının nedeni, gizli anahtarınızı açmadan doğru bir şeyi doğrulayabilmenizdir. Bu durumda, dijital imza, kullanıcının form gönderisi için ayarladığınız ilkeyi değiştirmesini önlemek için kullanılır.

Buradaki gibi dijital imzalar, web'in her yerinde güvenlik için kullanılır. Birisi (NSA?) Gerçekten onları kırabilseydi, S3 kovanızdan çok daha büyük hedefleri olurdu :)


2
ancak bir robot sınırsız sayıda dosyayı hızlı bir şekilde yüklemeyi deneyebilir. grup başına maksimum dosya politikası belirleyebilir miyim?
Dejell

3

Javascript tarayıcısından AWS S3'e dosya yüklemek ve S3 grubundaki tüm dosyaları listelemek için basit bir kod verdim.

Adımlar:

  1. Create IdentityPoolId oluşturmayı bilmek için http://docs.aws.amazon.com/cognito/latest/developerguide/identity-pools.html

    1. Konsol sayfasına gidin ve kova özelliklerinden çekirdek yapılandırmasını açın ve aşağıdaki XML kodunu buraya yazın.

      <?xml version="1.0" encoding="UTF-8"?>
      <CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
       <CORSRule>    
        <AllowedMethod>GET</AllowedMethod>
        <AllowedMethod>PUT</AllowedMethod>
        <AllowedMethod>DELETE</AllowedMethod>
        <AllowedMethod>HEAD</AllowedMethod>
        <AllowedHeader>*</AllowedHeader>
       </CORSRule>
      </CORSConfiguration>
    2. Aşağıdaki kodu içeren HTML dosyası oluşturun, kimlik bilgilerini değiştirin, dosyayı tarayıcıda açın ve keyfini çıkarın.

      <script type="text/javascript">
       AWS.config.region = 'ap-north-1'; // Region
       AWS.config.credentials = new AWS.CognitoIdentityCredentials({
       IdentityPoolId: 'ap-north-1:*****-*****',
       });
       var bucket = new AWS.S3({
       params: {
       Bucket: 'MyBucket'
       }
       });
      
       var fileChooser = document.getElementById('file-chooser');
       var button = document.getElementById('upload-button');
       var results = document.getElementById('results');
      
       function upload() {
       var file = fileChooser.files[0];
       console.log(file.name);
      
       if (file) {
       results.innerHTML = '';
       var params = {
       Key: n + '.pdf',
       ContentType: file.type,
       Body: file
       };
       bucket.upload(params, function(err, data) {
       results.innerHTML = err ? 'ERROR!' : 'UPLOADED.';
       });
       } else {
       results.innerHTML = 'Nothing to upload.';
       }    }
      </script>
      <body>
       <input type="file" id="file-chooser" />
       <input type="button" onclick="upload()" value="Upload to S3">
       <div id="results"></div>
      </body>

2
S3 grubuma dosya yüklemek için "IdentityPoolId" cihazımı kimse kullanamaz mı? Bu çözüm, herhangi bir 3. tarafın "IdentityPoolId" ürünümü kopyalamasını ve S3 grubuma çok sayıda dosya yüklemesini nasıl önler?
Sahil

1
stackoverflow.com/users/4535741/sahil Uygun CORS ayarlarını S3 grubuna ayarlayarak diğer alanlardan veri / dosya yüklenmesini engelleyebilirsiniz. Bu yüzden herhangi biri kimlik havuzu kimliğinize erişse bile, s3 dosyalarınızı değiştiremez.
Nilesh Pawar

2

Herhangi bir sunucu tarafı kodunuz yoksa, güvenliğiniz istemci tarafındaki JavaScript kodunuza erişimin güvenliğine bağlıdır (yani, kodu olan herkes bir şey yükleyebilir).

Bu yüzden, genel olarak yazılabilir (ancak okunabilir olmayan) özel bir S3 kovası oluşturmanızı öneririm, böylece istemci tarafında herhangi bir imzalı bileşene ihtiyacınız yoktur.

Grup adı (örneğin bir GUID) kötü amaçlı yüklemelere karşı tek savunmanız olacaktır (ancak potansiyel bir saldırgan, yalnızca ona yazdığı için veri aktarmak için grubunuzu kullanamaz)


1

Düğüm ve sunucusuz kullanarak bir ilke belgesi nasıl oluşturulur

"use strict";

const uniqid = require('uniqid');
const crypto = require('crypto');

class Token {

    /**
     * @param {Object} config SSM Parameter store JSON config
     */
    constructor(config) {

        // Ensure some required properties are set in the SSM configuration object
        this.constructor._validateConfig(config);

        this.region = config.region; // AWS region e.g. us-west-2
        this.bucket = config.bucket; // Bucket name only
        this.bucketAcl = config.bucketAcl; // Bucket access policy [private, public-read]
        this.accessKey = config.accessKey; // Access key
        this.secretKey = config.secretKey; // Access key secret

        // Create a really unique videoKey, with folder prefix
        this.key = uniqid() + uniqid.process();

        // The policy requires the date to be this format e.g. 20181109
        const date = new Date().toISOString();
        this.dateString = date.substr(0, 4) + date.substr(5, 2) + date.substr(8, 2);

        // The number of minutes the policy will need to be used by before it expires
        this.policyExpireMinutes = 15;

        // HMAC encryption algorithm used to encrypt everything in the request
        this.encryptionAlgorithm = 'sha256';

        // Client uses encryption algorithm key while making request to S3
        this.clientEncryptionAlgorithm = 'AWS4-HMAC-SHA256';
    }

    /**
     * Returns the parameters that FE will use to directly upload to s3
     *
     * @returns {Object}
     */
    getS3FormParameters() {
        const credentialPath = this._amazonCredentialPath();
        const policy = this._s3UploadPolicy(credentialPath);
        const policyBase64 = new Buffer(JSON.stringify(policy)).toString('base64');
        const signature = this._s3UploadSignature(policyBase64);

        return {
            'key': this.key,
            'acl': this.bucketAcl,
            'success_action_status': '201',
            'policy': policyBase64,
            'endpoint': "https://" + this.bucket + ".s3-accelerate.amazonaws.com",
            'x-amz-algorithm': this.clientEncryptionAlgorithm,
            'x-amz-credential': credentialPath,
            'x-amz-date': this.dateString + 'T000000Z',
            'x-amz-signature': signature
        }
    }

    /**
     * Ensure all required properties are set in SSM Parameter Store Config
     *
     * @param {Object} config
     * @private
     */
    static _validateConfig(config) {
        if (!config.hasOwnProperty('bucket')) {
            throw "'bucket' is required in SSM Parameter Store Config";
        }
        if (!config.hasOwnProperty('region')) {
            throw "'region' is required in SSM Parameter Store Config";
        }
        if (!config.hasOwnProperty('accessKey')) {
            throw "'accessKey' is required in SSM Parameter Store Config";
        }
        if (!config.hasOwnProperty('secretKey')) {
            throw "'secretKey' is required in SSM Parameter Store Config";
        }
    }

    /**
     * Create a special string called a credentials path used in constructing an upload policy
     *
     * @returns {String}
     * @private
     */
    _amazonCredentialPath() {
        return this.accessKey + '/' + this.dateString + '/' + this.region + '/s3/aws4_request';
    }

    /**
     * Create an upload policy
     *
     * @param {String} credentialPath
     *
     * @returns {{expiration: string, conditions: *[]}}
     * @private
     */
    _s3UploadPolicy(credentialPath) {
        return {
            expiration: this._getPolicyExpirationISODate(),
            conditions: [
                {bucket: this.bucket},
                {key: this.key},
                {acl: this.bucketAcl},
                {success_action_status: "201"},
                {'x-amz-algorithm': 'AWS4-HMAC-SHA256'},
                {'x-amz-credential': credentialPath},
                {'x-amz-date': this.dateString + 'T000000Z'}
            ],
        }
    }

    /**
     * ISO formatted date string of when the policy will expire
     *
     * @returns {String}
     * @private
     */
    _getPolicyExpirationISODate() {
        return new Date((new Date).getTime() + (this.policyExpireMinutes * 60 * 1000)).toISOString();
    }

    /**
     * HMAC encode a string by a given key
     *
     * @param {String} key
     * @param {String} string
     *
     * @returns {String}
     * @private
     */
    _encryptHmac(key, string) {
        const hmac = crypto.createHmac(
            this.encryptionAlgorithm, key
        );
        hmac.end(string);

        return hmac.read();
    }

    /**
     * Create an upload signature from provided params
     * https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html#signing-request-intro
     *
     * @param policyBase64
     *
     * @returns {String}
     * @private
     */
    _s3UploadSignature(policyBase64) {
        const dateKey = this._encryptHmac('AWS4' + this.secretKey, this.dateString);
        const dateRegionKey = this._encryptHmac(dateKey, this.region);
        const dateRegionServiceKey = this._encryptHmac(dateRegionKey, 's3');
        const signingKey = this._encryptHmac(dateRegionServiceKey, 'aws4_request');

        return this._encryptHmac(signingKey, policyBase64).toString('hex');
    }
}

module.exports = Token;

Kullanılan yapılandırma nesnesi SSM Parametre Deposunda saklanır ve şöyle görünür

{
    "bucket": "my-bucket-name",
    "region": "us-west-2",
    "bucketAcl": "private",
    "accessKey": "MY_ACCESS_KEY",
    "secretKey": "MY_SECRET_ACCESS_KEY",
}

0

Üçüncü taraf bir hizmet kullanmak istiyorsanız, auth0.com bu entegrasyonu destekler. Auth0 hizmeti, bir AWS geçici oturum jetonu için üçüncü taraf bir TOA hizmeti kimlik doğrulaması değiştirerek izinleri sınırlı olacaktır.

Bkz. Https://github.com/auth0-samples/auth0-s3-sample/
ve auth0 dokümanları.


1
Anladığım kadarıyla - şimdi bunun için Cognito var mı?
Vitaly Zdanevich
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.