[PHP, Oracle, Облачные вычисления] Создаём мини PHP SDK для подписи запросов к Oracle Cloud Infrastructure API
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Идея написать эту библиотеку возникла, когда захотелось в полной мере воспользоваться всеми преимуществами бесплатного предложения Oracle Cloud Infrastructure, а именно 10 ГБ хранилища объектов (Object Storage) и 10 ТБ исходящего трафика в месяц. Разница с AWS S3 просто огромнейшая. К сожалению, Oracle Cloud не имеет в наличии SDK для всё еще самого популярного языка программирования для разработки веб-сайтов. Хорошая новость состоит в том, что сервис частично совместим с Amazon S3, а это означает, что можно применить уже имеющиеся и отлично задокументированные инструменты разработчика, в том числе для PHP.Тем, кому не терпится увидеть код, добро пожаловать https://github.com/hitrov/oci-api-php-request-sign.Действительно, с имеющимися инструментами можно выполнять почти все операции, которые можно представить - для создания, чтения и удаления корзин (buckets) и объектов (файлов). Корзины могут быть как публичными (с возможностью листинга файлов и без) и приватными. Есть возможность загружать файлы в приватную корзину, имея лишь «секретный» URL (сгенерированный вручную с помощью CLI или веб-интерфейса - консоли Oracle Cloud). На самом деле этого уже может быть достаточно для многих сценариев, особенно если генерировать стойкие к подбору имена файлов - в случае, если вы не хотите выставлять их на публику.Меня интересовала возможность «расшаривать» файлы, то есть делиться общедоступными ссылками на файлы, и, конечно же, ограничивать доступ при необходимости. При небольшом количестве файлов можно делать это вручную, но мы собрались здесь, чтобы иметь программный доступ. В AWS S3 это называется Pre-Signed URL, а у Oracle - Pre-Authenticated Request.Установка AWS PHP SDK
composer require aws/aws-sdk-php
Ниже будет показано, где взять доступы (AWS_ACCESS_KEY_IDи AWS_SECRET_ACCESS_KEY.Namespace же можно увидеть
$namespaceName = 'frpegp***';
$bucketName = 'test******05';
$region = 'eu-frankfurt-1';
$endpoint = "https://$namespaceName.compat.objectstorage.$region.oraclecloud.com";
$s3 = new Aws\S3\S3Client([
'version' => 'latest',
'region' => $region,
'endpoint' => $endpoint,
'signature_version' => 'v4',
'use_path_style_endpoint' => true,
'credentials' => [
'key' => 'AKI***YYJ', // remove if you have env var AWS_ACCESS_KEY_ID
'secret' => 'ndK***cIf', , // remove if you have env var AWS_SECRET_ACCESS_KEY
],
]);
$cmd = $s3->getCommand('GetObject', [
'Bucket' => $bucketName,
'Key' => 'fff.txt'
]);
$request = $s3->createPresignedRequest($cmd, '+20 minutes');
К сожалению, данная операция, хотя и не вызывает ошибку, отдавая в ответ PSR-7 request, но возвращаемый им URL видаhttps://{namespace}.compat.objectstorage.eu-frankfurt-1.oraclecloud.com/{bucket}/fff.txt?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=***%2F20210210%2Feu-frankfurt-1%2Fs3%2Faws4_request&X-Amz-Date=20210210T185244Z&X-Amz-SignedHeaders=host&X-Amz-Expires=1200&X-Amz-Signature=a167a***9a857просто не работает.
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Message>The required information to complete authentication was not provided.</Message>
<Code>SignatureDoesNotMatch</Code>
</Error>
Поскольку совместимость для данной операции не была заявлена, странно было бы ожидать иного, но попробовать стоило :)Здесь я попробую очень кратко описать, что необходимо для подписи запроса к API, ведь все изложено довольно подробно здесь, пусть и с примерами для иных языков программирования.Разумеется, подпись будет работать для всех запросов начиная от создания\остановки\бэкапа автономной базы данных, управления DNS и заканчивая отправкой Email. Всё что указано в API Reference and Endpoints.Прежде всего, для того, чтобы начать работу, нужны ключи доступа, в веб-интерфейсе (консоли) Oracle Cloud необходимо зайти в User Settings
Действия в профиле Oracle CloudAPI Keys — Add API Key
API Keys - Add API KeyDownload private key (сохраняем в надежном месте), затем Add
Download Private Key and AddСохраняем все значения из текстового поля, они нам понадобятся через минуту
Configuration File exampleДля того, чтобы воспользоваться AWS PHP SDK, вам необходимы Customer Secret Keys (они же AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY в понимании Amazon.
Установка Oracle Cloud Infrastructure mini PHP SDK (никаких внешних зависимостей!)
composer require hitrov/oci-api-php-request-sign
Пакет использует стандартную PSR-4 автозагрузку классов.
require 'vendor/autoload.php';
use Hitrov\OCI\Signer;
Для авторизации нужно задать переменные среды (замените на значения, взятые из текстового поля, проставьте путь к файлу с приватным ключом).
OCI_TENANCY_ID=ocid1.tenancy.oc1..aaaaaaaaba3pv6wkcr4jqae5f15p2b2m2yt2j6rx32uzr4h25vqstifsfdsq
OCI_USER_ID=ocid1.user.oc1..aaaaaaaat5nvwcna5j6aqzjcaty5eqbb6qt2jvpkanghtgdaqedqw3rynjq
OCI_KEY_FINGERPRINT=20:3b:97:13:55:1c:5b:0d:d3:37:d8:50:4e:c5:3a:34
OCI_PRIVATE_KEY_FILENAME=/path/to/privatekey.pem
В этом случае конструктор не принимает аргументов.
$signer = new Signer;
Переменным среды есть несколько альтернатив https://github.com/hitrov/oci-api-php-request-sign#alternatives-for-providing-credentials , не стану дублировать это здесь.Мы попробуем выполнить CreatePreauthenticatedRequest.Вся сложность (если можно так выразиться) абстрагирована в один публичный метод
public function getHeaders(
string $url, string $method = 'GET', ?string $body = null, ?string $contentType = 'application/json', string $dateString = null
): array
Пример использования
$curl = curl_init();
$url = 'https://objectstorage.eu-frankfurt-1.oraclecloud.com/n/{namespaceName}/b/{bucketName}/p/';
$method = 'POST';
$body = '{"accessType": "ObjectRead", "name": "read-access-to-image.png", "objectName": "path/to/image.png", "timeExpires": "2021-03-01T00:00:00-00:00"}';
$headers = $signer->getHeaders($url, $method, $body, 'application/json');
var_dump($headers);
$curlOptions = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 5,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
];
if ($body) {
// not needed for GET or HEAD requests
$curlOptions[CURLOPT_POSTFIELDS] = $body;
}
curl_setopt_array($curl, $curlOptions);
$response = curl_exec($curl);
echo $response;
curl_close($curl);
array(6) {
[0]=>
string(35) "date: Mon, 08 Feb 2021 20:49:22 GMT"
[1]=>
string(50) "host: objectstorage.eu-frankfurt-1.oraclecloud.com"
[2]=>
string(18) "content-length: 76"
[3]=>
string(30) "content-type: application/json"
[4]=>
string(62) "x-content-sha256: X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE="
[5]=>
string(538) "Authorization: Signature version="1",keyId="ocid1.tenancy.oc1..aaaaaaaaba3pv6wkcr4jqae5f15p2b2m2yt2j6rx32uzr4h25vqstifsfdsq/ocid1.user.oc1..aaaaaaaat5nvwcna5j6aqzjcaty5eqbb6qt2jvpkanghtgdaqedqw3rynjq/20:3b:97:13:55:1c:5b:0d:d3:37:d8:50:4e:c5:3a:34",algorithm="rsa-sha256",headers="date (request-target) host content-length content-type x-content-sha256",signature="LXWXDA8VmXXc1NRbMmXtW61IS97DfIOMAnlj+Gm+oBPNc2svXYdhcXNJ+oFPoi9qJHLnoUiHqotTzuVPXSG5iyXzFntvkAn3lFIAja52iwwwcJflEIXj/b39eG2dCsOTmmUJguut0FsLhCRSX0eylTSLgxTFGoQi7K/m18nafso=""
}
{
"accessUri": "/p/AlIlOEsMok7oE7YkN30KJUDjDKQjk493BKbuM-ANUNGdBBAHzHT_5lFlzYC9CQiA/n/{namespaceName}/b/{bucketName}/o/path/to/image.png",
"id": "oHJQWGxpD+2PhDqtoewvLCf8/lYNlaIpbZHYx+mBryAad/q0LnFy37Me/quKhxEi:path/to/image.png",
"name": "read-access-to-image.png",
"accessType": "ObjectRead",
"objectName": "path/to/image.png",
"timeCreated": "2021-02-09T11:52:45.053Z",
"timeExpires": "2021-03-01T00:00:00Z"
}
Вот и всё!По большому счету, клиентский код более ни в чем не нуждается. Остальное для тех, кому любопытно – в образовательных целях.1) Прежде всего, нам необходимо собрать список «подписываемых заголовков» (SIGNING_HEADERS_NAMES). Он всегда содержит
- date
- · (request-target)
- · host
Для POST|PUT|PATCH запросов добавляются еще три
- · content-length
- · content-type
- · x-content-sha256
$signingHeadersNames = $signer->getSigningHeadersNames('POST');
2) SHA256 хэш «тела» запроса – кодированный в base64
$bodyHashBase64 = $signer->getBodyHashBase64($body);
3) Сформировать строку для подписи, в нашем случае она будет выглядеть следующим образом
date: Mon, 08 Feb 2021 20:51:33 GMT
(request-target): post /n/{namespaceName}/b/{bucketName}/p/
host: objectstorage.eu-frankfurt-1.oraclecloud.com
content-length: 76
content-type: application/json
x-content-sha256: X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=
$signingString = $signer->getSigningString($url, $method, $body, 'application/json');
Хэш мы получили в (2). Важно, что дата и время не должны отличаться от текущих на более, чем 5 минут.4) Подписать строку из (3) приватным ключом с помощью алгоритма RSA-SHA256
$signature = $signer->calculateSignature($signingString, $privateKeyString);
5) Сформировать KEY_ID данными, которые вы скопировали при создании API Key, это строка, разделенная слешами "{OCITENANCYID}/{OCIUSERID}/{OCIKEY_FINGERPRINT}"
$keyId = $signer->getKeyId();
6) Теперь мы готовы сгенерировать заголовок авторизации (версия 1останется таковой до отдельного уведомления от Oracle)Authorization: Signature version="1",keyId="{KEY_ID}",algorithm="rsa-sha256",headers="{SIGNING_HEADERS_NAMES_STRING}",signature="{SIGNATURE}"где SIGNING_HEADERS_NAMES_STRING – это список из (1), разделенный пробелами.date (request-target) host content-length content-type x-content-sha256
$signingHeadersNamesString = implode(' ', $signingHeadersNames);
$authorizationHeader = $signer->getAuthorizationHeader($keyId, $signingHeadersNamesString, $signature);
Пример вывода
Authorization: Signature version="1",keyId="ocid1.tenancy.oc1..aaaaaaaaba3pv6wkcr4jqae5f15p2b2m2yt2j6rx32uzr4h25vqstifsfdsq/ocid1.user.oc1..aaaaaaaat5nvwcna5j6aqzjcaty5eqbb6qt2jvpkanghtgdaqedqw3rynjq/20:3b:97:13:55:1c:5b:0d:d3:37:d8:50:4e:c5:3a:34",algorithm="rsa-sha256",headers="date (request-target) host content-length content-type x-content-sha256",signature="LXWXDA8VmXXc1NRbMmXtW61IS97DfIOMAnlj+Gm+oBPNc2svXYdhcXNJ+oFPoi9qJHLnoUiHqotTzuVPXSG5iyXzFntvkAn3lFIAja52iwwwcJflEIXj/b39eG2dCsOTmmUJguut0FsLhCRSX0eylTSLgxTFGoQi7K/m18nafso="
Реальные заголовки запроса - см. вывод var_dump()выше - должны содержать всё из (3), за исключением поля (request-target) и его значения. И, конечно же, заголовок авторизации (6).Мне помогла статья Oracle Cloud Infrastructure (OCI) REST call walkthrough with curl. Некоторые имена методов позаимствованы из официального GoLang SDK. Тест-кейсы – оттуда же.
===========
Источник:
habr.com
===========
Похожие новости:
- [SQL, NoSQL, API, ООП, Хранилища данных] Четыре API для базы данных
- [Oracle, SQL] Oracle: Deterministic functions, result_cache and operators
- [Разработка веб-сайтов, PHP, Программирование] Tagged Unions в PHP (примерно как в Rust)
- [Open source, PHP, Symfony, Yii, Laravel] Package Wizard — незаменимый помощник при разработке веб-приложений
- [Разработка веб-сайтов, JavaScript, Программирование, .NET] Рабочий прототип секретного мессенджера
- [Тестирование IT-систем, Тестирование веб-сервисов, Тестирование мобильных приложений] Мобильное тестирование, автоматизация тестирования, тестирование API: с чем нужно уметь работать в 2021 году
- [Облачные вычисления, Серверное администрирование, Облачные сервисы] Что такое serverless computing (бессерверные вычисления)? (перевод)
- [Системное администрирование, Oracle, Облачные сервисы, Лайфхаки для гиков] Получаем бесплатные сервера в Oracle Cloud Free Tier
- [Разработка мобильных приложений, Разработка под Android] Новости Android разработки #3
- [JavaScript, API] ExtendScript Работа с файлами
Теги для поиска: #_php, #_oracle, #_oblachnye_vychislenija (Облачные вычисления), #_php, #_sdk, #_oracle, #_oracle_cloud, #_api, #_requests, #_php, #_oracle, #_oblachnye_vychislenija (
Облачные вычисления
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:31
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Идея написать эту библиотеку возникла, когда захотелось в полной мере воспользоваться всеми преимуществами бесплатного предложения Oracle Cloud Infrastructure, а именно 10 ГБ хранилища объектов (Object Storage) и 10 ТБ исходящего трафика в месяц. Разница с AWS S3 просто огромнейшая. К сожалению, Oracle Cloud не имеет в наличии SDK для всё еще самого популярного языка программирования для разработки веб-сайтов. Хорошая новость состоит в том, что сервис частично совместим с Amazon S3, а это означает, что можно применить уже имеющиеся и отлично задокументированные инструменты разработчика, в том числе для PHP.Тем, кому не терпится увидеть код, добро пожаловать https://github.com/hitrov/oci-api-php-request-sign.Действительно, с имеющимися инструментами можно выполнять почти все операции, которые можно представить - для создания, чтения и удаления корзин (buckets) и объектов (файлов). Корзины могут быть как публичными (с возможностью листинга файлов и без) и приватными. Есть возможность загружать файлы в приватную корзину, имея лишь «секретный» URL (сгенерированный вручную с помощью CLI или веб-интерфейса - консоли Oracle Cloud). На самом деле этого уже может быть достаточно для многих сценариев, особенно если генерировать стойкие к подбору имена файлов - в случае, если вы не хотите выставлять их на публику.Меня интересовала возможность «расшаривать» файлы, то есть делиться общедоступными ссылками на файлы, и, конечно же, ограничивать доступ при необходимости. При небольшом количестве файлов можно делать это вручную, но мы собрались здесь, чтобы иметь программный доступ. В AWS S3 это называется Pre-Signed URL, а у Oracle - Pre-Authenticated Request.Установка AWS PHP SDK composer require aws/aws-sdk-php
$namespaceName = 'frpegp***';
$bucketName = 'test******05'; $region = 'eu-frankfurt-1'; $endpoint = "https://$namespaceName.compat.objectstorage.$region.oraclecloud.com"; $s3 = new Aws\S3\S3Client([ 'version' => 'latest', 'region' => $region, 'endpoint' => $endpoint, 'signature_version' => 'v4', 'use_path_style_endpoint' => true, 'credentials' => [ 'key' => 'AKI***YYJ', // remove if you have env var AWS_ACCESS_KEY_ID 'secret' => 'ndK***cIf', , // remove if you have env var AWS_SECRET_ACCESS_KEY ], ]); $cmd = $s3->getCommand('GetObject', [ 'Bucket' => $bucketName, 'Key' => 'fff.txt' ]); $request = $s3->createPresignedRequest($cmd, '+20 minutes'); <?xml version="1.0" encoding="UTF-8"?>
<Error> <Message>The required information to complete authentication was not provided.</Message> <Code>SignatureDoesNotMatch</Code> </Error> Действия в профиле Oracle CloudAPI Keys — Add API Key API Keys - Add API KeyDownload private key (сохраняем в надежном месте), затем Add Download Private Key and AddСохраняем все значения из текстового поля, они нам понадобятся через минуту Configuration File exampleДля того, чтобы воспользоваться AWS PHP SDK, вам необходимы Customer Secret Keys (они же AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY в понимании Amazon. Установка Oracle Cloud Infrastructure mini PHP SDK (никаких внешних зависимостей!) composer require hitrov/oci-api-php-request-sign
require 'vendor/autoload.php';
use Hitrov\OCI\Signer; OCI_TENANCY_ID=ocid1.tenancy.oc1..aaaaaaaaba3pv6wkcr4jqae5f15p2b2m2yt2j6rx32uzr4h25vqstifsfdsq
OCI_USER_ID=ocid1.user.oc1..aaaaaaaat5nvwcna5j6aqzjcaty5eqbb6qt2jvpkanghtgdaqedqw3rynjq OCI_KEY_FINGERPRINT=20:3b:97:13:55:1c:5b:0d:d3:37:d8:50:4e:c5:3a:34 OCI_PRIVATE_KEY_FILENAME=/path/to/privatekey.pem $signer = new Signer;
public function getHeaders(
string $url, string $method = 'GET', ?string $body = null, ?string $contentType = 'application/json', string $dateString = null ): array $curl = curl_init();
$url = 'https://objectstorage.eu-frankfurt-1.oraclecloud.com/n/{namespaceName}/b/{bucketName}/p/'; $method = 'POST'; $body = '{"accessType": "ObjectRead", "name": "read-access-to-image.png", "objectName": "path/to/image.png", "timeExpires": "2021-03-01T00:00:00-00:00"}'; $headers = $signer->getHeaders($url, $method, $body, 'application/json'); var_dump($headers); $curlOptions = [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => '', CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 5, CURLOPT_FOLLOWLOCATION => true, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, ]; if ($body) { // not needed for GET or HEAD requests $curlOptions[CURLOPT_POSTFIELDS] = $body; } curl_setopt_array($curl, $curlOptions); $response = curl_exec($curl); echo $response; curl_close($curl); array(6) {
[0]=> string(35) "date: Mon, 08 Feb 2021 20:49:22 GMT" [1]=> string(50) "host: objectstorage.eu-frankfurt-1.oraclecloud.com" [2]=> string(18) "content-length: 76" [3]=> string(30) "content-type: application/json" [4]=> string(62) "x-content-sha256: X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=" [5]=> string(538) "Authorization: Signature version="1",keyId="ocid1.tenancy.oc1..aaaaaaaaba3pv6wkcr4jqae5f15p2b2m2yt2j6rx32uzr4h25vqstifsfdsq/ocid1.user.oc1..aaaaaaaat5nvwcna5j6aqzjcaty5eqbb6qt2jvpkanghtgdaqedqw3rynjq/20:3b:97:13:55:1c:5b:0d:d3:37:d8:50:4e:c5:3a:34",algorithm="rsa-sha256",headers="date (request-target) host content-length content-type x-content-sha256",signature="LXWXDA8VmXXc1NRbMmXtW61IS97DfIOMAnlj+Gm+oBPNc2svXYdhcXNJ+oFPoi9qJHLnoUiHqotTzuVPXSG5iyXzFntvkAn3lFIAja52iwwwcJflEIXj/b39eG2dCsOTmmUJguut0FsLhCRSX0eylTSLgxTFGoQi7K/m18nafso="" } {
"accessUri": "/p/AlIlOEsMok7oE7YkN30KJUDjDKQjk493BKbuM-ANUNGdBBAHzHT_5lFlzYC9CQiA/n/{namespaceName}/b/{bucketName}/o/path/to/image.png", "id": "oHJQWGxpD+2PhDqtoewvLCf8/lYNlaIpbZHYx+mBryAad/q0LnFy37Me/quKhxEi:path/to/image.png", "name": "read-access-to-image.png", "accessType": "ObjectRead", "objectName": "path/to/image.png", "timeCreated": "2021-02-09T11:52:45.053Z", "timeExpires": "2021-03-01T00:00:00Z" }
$signingHeadersNames = $signer->getSigningHeadersNames('POST');
$bodyHashBase64 = $signer->getBodyHashBase64($body);
date: Mon, 08 Feb 2021 20:51:33 GMT
(request-target): post /n/{namespaceName}/b/{bucketName}/p/ host: objectstorage.eu-frankfurt-1.oraclecloud.com content-length: 76 content-type: application/json x-content-sha256: X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE= $signingString = $signer->getSigningString($url, $method, $body, 'application/json');
$signature = $signer->calculateSignature($signingString, $privateKeyString);
$keyId = $signer->getKeyId();
$signingHeadersNamesString = implode(' ', $signingHeadersNames);
$authorizationHeader = $signer->getAuthorizationHeader($keyId, $signingHeadersNamesString, $signature); Authorization: Signature version="1",keyId="ocid1.tenancy.oc1..aaaaaaaaba3pv6wkcr4jqae5f15p2b2m2yt2j6rx32uzr4h25vqstifsfdsq/ocid1.user.oc1..aaaaaaaat5nvwcna5j6aqzjcaty5eqbb6qt2jvpkanghtgdaqedqw3rynjq/20:3b:97:13:55:1c:5b:0d:d3:37:d8:50:4e:c5:3a:34",algorithm="rsa-sha256",headers="date (request-target) host content-length content-type x-content-sha256",signature="LXWXDA8VmXXc1NRbMmXtW61IS97DfIOMAnlj+Gm+oBPNc2svXYdhcXNJ+oFPoi9qJHLnoUiHqotTzuVPXSG5iyXzFntvkAn3lFIAja52iwwwcJflEIXj/b39eG2dCsOTmmUJguut0FsLhCRSX0eylTSLgxTFGoQi7K/m18nafso="
=========== Источник: habr.com =========== Похожие новости:
Облачные вычисления ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:31
Часовой пояс: UTC + 5