صورة عامل إرساء 5.94 متر مع Telegram MTProxy

كما سمع الجميع بالفعل ، في نهاية مايو أطلقت Telegram خادم MTProto Proxy الرسمي (المعروف أيضًا باسم MTProxy) ، المكتوب باللغة التالية . في عام 2018 ، لا يوجد الكثير بدون Docker ، لأنه مصحوب بنفس الطريقة "الرسمية" بتنسيق صفر التكوين. كل شيء سيكون على ما يرام ، لكن ثلاثة "استثناءات" أفسدت انطباع الإصدار قليلاً: الصورة تزن> 130 ميجا بايت (هناك دبيان ممتلئ الجسم ، وليس جبال الألب المعتادة) ، بسبب "التكوين صفر" لا يتم تكوينه بشكل ملائم دائمًا (فقط من خلال إعدادات البيئة) ونسي الرجال الحملة ، ووضعوا ملف Dockerfile.



TL ؛ DR سنقوم بعمل صورة عامل إرساء رسمي على أساس جبال الألب عمليا 1 في 1 بحجم 5.94 ميجا بايت ووضعها هنا (وملف Dockerfile هنا ) ؛ على طول الطريق ، سنكتشف كيف يمكنك في بعض الأحيان تكوين صداقات مع برنامج Alpine باستخدام القصاصات والملف ، وسنلعب قليلاً في الحجم ، حصريًا من أجل المتعة.

محتوى الصورة


مرة أخرى ، بسبب كل هذه الضجة؟ دعونا نرى ما تمثله الصورة الرسمية مع الأمر history :

$ docker history --no-trunc --format "{{.Size}}\t{{.CreatedBy}}" telegrammessenger/proxy 

تتم قراءة الطبقات من الأسفل إلى الأعلى على التوالي:



أثقلها هو Debian Jessie ، الذي ورثت منه الصورة الأصلية ، يجب أن نتخلص منه أولاً (alpine: 3.6 ، للمقارنة ، يزن 3.97 ميغابايت) ؛ متبوعة بأبعاد هي حليقة وشهادات جديدة. لفهم ما يعنيه الملفان الآخران والدليل ، سننظر في الداخل باستخدام الأمر run ، واستبدال CMD بـ bash (سيسمح لك هذا بالتجول في الصورة التي تم إطلاقها ، والتعرف على بعضكما عن كثب ، وتشغيل أجزاء معينة ، ونسخ شيء مفيد):

 $ docker run -it --rm telegrammessenger/proxy /bin/bash 

الآن يمكننا بسهولة استعادة صورة الحادث - شيء يشبه ملف Dockerfile الرسمي المفقود:

 FROM debian:jessie-20180312 RUN set -eux \ && apt-get update \ && apt-get install -y --no-install-recommends curl ca-certificates \ && rm -rf /var/lib/apt/lists/* COPY ./mtproto-proxy /usr/local/bin RUN mkdir /data COPY ./secret/ /etc/telegram/ COPY ./run.sh /run.sh CMD ["/bin/sh", "-c", "/bin/bash /run.sh"] 

عندما يكون mtproto-proxy خادمًا مترجمًا ، يحتوي المجلد السري فقط على ملف hello-explorers-how-you-do-do مع مفتاح التشفير AES (انظر أوامر الخادم ، هناك بالمناسبة ، هناك توصية رسمية للحصول على المفتاح من خلال واجهة برمجة التطبيقات ، ولكن ضعه على هذا النحو ربما لتجنب الموقف عندما يتم حظر واجهة برمجة التطبيقات أيضًا) ، ويقوم run.sh بكل الاستعدادات لبدء الوكيل.

محتويات run.sh الأصلي
 #!/bin/bash if [ ! -z "$DEBUG" ]; then set -x; fi mkdir /data 2>/dev/null >/dev/null RANDOM=$(printf "%d" "0x$(head -c4 /dev/urandom | od -t x1 -An | tr -d ' ')") if [ -z "$WORKERS" ]; then WORKERS=2 fi echo "####" echo "#### Telegram Proxy" echo "####" echo SECRET_CMD="" if [ ! -z "$SECRET" ]; then echo "[+] Using the explicitly passed secret: '$SECRET'." elif [ -f /data/secret ]; then SECRET="$(cat /data/secret)" echo "[+] Using the secret in /data/secret: '$SECRET'." else if [[ ! -z "$SECRET_COUNT" ]]; then if [[ ! ( "$SECRET_COUNT" -ge 1 && "$SECRET_COUNT" -le 16 ) ]]; then echo "[F] Can generate between 1 and 16 secrets." exit 5 fi else SECRET_COUNT="1" fi echo "[+] No secret passed. Will generate $SECRET_COUNT random ones." SECRET="$(dd if=/dev/urandom bs=16 count=1 2>&1 | od -tx1 | head -n1 | tail -c +9 | tr -d ' ')" for pass in $(seq 2 $SECRET_COUNT); do SECRET="$SECRET,$(dd if=/dev/urandom bs=16 count=1 2>&1 | od -tx1 | head -n1 | tail -c +9 | tr -d ' ')" done fi if echo "$SECRET" | grep -qE '^[0-9a-fA-F]{32}(,[0-9a-fA-F]{32}){,15}$'; then SECRET="$(echo "$SECRET" | tr '[:upper:]' '[:lower:]')" SECRET_CMD="-S $(echo "$SECRET" | sed 's/,/ -S /g')" echo -- "$SECRET_CMD" > /data/secret_cmd echo "$SECRET" > /data/secret else echo '[F] Bad secret format: should be 32 hex chars (for 16 bytes) for every secret; secrets should be comma-separated.' exit 1 fi if [ ! -z "$TAG" ]; then echo "[+] Using the explicitly passed tag: '$TAG'." fi TAG_CMD="" if [[ ! -z "$TAG" ]]; then if echo "$TAG" | grep -qE '^[0-9a-fA-F]{32}$'; then TAG="$(echo "$TAG" | tr '[:upper:]' '[:lower:]')" TAG_CMD="-P $TAG" else echo '[!] Bad tag format: should be 32 hex chars (for 16 bytes).' echo '[!] Continuing.' fi fi curl -s https://core.telegram.org/getProxyConfig -o /etc/telegram/backend.conf || { echo '[F] Cannot download proxy configuration from Telegram servers.' exit 2 } CONFIG=/etc/telegram/backend.conf IP="$(curl -s -4 "https://digitalresistance.dog/myIp")" INTERNAL_IP="$(ip -4 route get 8.8.8.8 | grep '^8\.8\.8\.8\s' | grep -Po 'src\s+\d+\.\d+\.\d+\.\d+' | awk '{print $2}')" if [[ -z "$IP" ]]; then echo "[F] Cannot determine external IP address." exit 3 fi if [[ -z "$INTERNAL_IP" ]]; then echo "[F] Cannot determine internal IP address." exit 4 fi echo "[*] Final configuration:" I=1 echo "$SECRET" | tr ',' '\n' | while read S; do echo "[*] Secret $I: $S" echo "[*] tg:// link for secret $I auto configuration: tg://proxy?server=${IP}&port=443&secret=${S}" echo "[*] t.me link for secret $I: https://t.me/proxy?server=${IP}&port=443&secret=${S}" I=$(($I+1)) done [ ! -z "$TAG" ] && echo "[*] Tag: $TAG" || echo "[*] Tag: no tag" echo "[*] External IP: $IP" echo "[*] Make sure to fix the links in case you run the proxy on a different port." echo echo '[+] Starting proxy...' sleep 1 exec /usr/local/bin/mtproto-proxy -p 2398 -H 443 -M "$WORKERS" -C 60000 --aes-pwd /etc/telegram/hello-explorers-how-are-you-doing -u root $CONFIG --allow-skip-dh --nat-info "$INTERNAL_IP:$IP" $SECRET_CMD $TAG_CMD 

التجمع


تحت CentOS 7 MTProxy على Habré التي تم جمعها بالفعل ، سنحاول جمع صورة تحت Alpine وحفظ 130 ميغابايت ، الإعلانات التجارية ، 130 في صورة عامل الميناء الناتج.

السمة المميزة لـ Alpine Linux هي استخدام musl بدلاً من glibc. كلاهما مكتبات C القياسية. Musl صغير (ليس لديه خمس "المعيار" فيه) ، لكن الحجم والأداء (الموعود على الأقل) يقرران متى يتعلق الأمر بـ Docker. إن وضع glibc على جبال الألب ليس صحيحًا من الناحية العرقية ، ولن يفهم العم جاكوب جوتيركا ، على سبيل المثال.

سنقوم أيضًا ببناء عامل ميناء لعزل التبعيات والحصول على حرية التجريب ، لذلك قم بإنشاء ملف Dockerfile جديد:

 FROM alpine:3.6 RUN apk add --no-cache git make gcc musl-dev linux-headers openssl-dev RUN git clone --single-branch --depth 1 https://github.com/TelegramMessenger/MTProxy.git /mtproxy/sources RUN cd /mtproxy/sources \ && make -j$(getconf _NPROCESSORS_ONLN) 

من التبعيات ، سيكون git في متناول اليد (وليس فقط لاستنساخ المستودع الرسمي ، سيحدد ملف make sha التزامًا بالإصدار) ، وإنشاء ، وملفات gcc ، ورأس (تم الحصول على الحد الأدنى من المجموعة تجريبيًا). نحن نستنسخ الفرع الرئيسي فقط بعمق التزام واحد (بالتأكيد لا نحتاج إلى التاريخ). حسنًا ، دعنا نحاول استخدام جميع موارد المضيف عند الترجمة باستخدام -j. قمت بتقسيمها عمداً إلى عدد أكبر من الطبقات من أجل الحصول على ذاكرة تخزين مؤقت مناسبة أثناء إعادة البناء (عادة ما يكون هناك الكثير منها).

سنقوم بتشغيل أمر البناء (يجري في الدليل مع ملف Dockerfile):

 $ docker build -t mtproxy:test . 

وهذه هي المشكلة الأولى:

 In file included from ./net/net-connections.h:34:0, from mtproto/mtproto-config.c:44: ./jobs/jobs.h:234:23: error: field 'rand_data' has incomplete type struct drand48_data rand_data; ^~~~~~~~~ 

في الواقع ، سيتم ربط جميع تلك اللاحقة به. أولاً ، بالنسبة لأولئك الذين ليسوا على دراية بأنفسهم ، فإن المترجم يقسم بالفعل على عدم وجود إعلان عن بنية بيانات drand48_data. ثانياً ، سجل مطورو musl وظائف عشوائية آمنة باستخدام الخيط (مع postr _r) وعلى كل ما يتعلق بها (بما في ذلك الهياكل). كما أن مطوري Telegram ، بدورهم ، لم يكلفوا أنفسهم عناء تجميع الأنظمة التي لم يتم تنفيذ عشوائي_r ونظرائها (في العديد من مكتبات أنظمة التشغيل ، يمكنك رؤية إشارة HAVE_RANDOM_R أو وجودها التعسفي أو غيابها لهذه المجموعة من الوظائف عادة ما يؤخذ في الاعتبار في التكوين التلقائي).

حسنًا ، الآن سنقوم بالتأكيد بتثبيت glibc؟ لا! سنقوم بنسخ ما نحتاجه من glibc إلى الحد الأدنى ونقوم بعمل تصحيح لمصادر MTProxy.

بالإضافة إلى المشاكل المتعلقة بـ random_r ، فإننا نواجه مشكلة في وظيفة backtrace (execinfo.h) ، والتي تُستخدم لإخراج مكدس backtrace في حالة وجود استثناء: يمكنك محاولة استبدالها بالتنفيذ من libunwind ، ولكن الأمر لا يستحق ذلك ، لأنه تم تأطير المكالمة عن طريق التحقق من __GLIBC__.

Random_compat.patch محتويات التصحيح
 Index: jobs/jobs.h IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- jobs/jobs.h (revision cdd348294d86e74442bb29bd6767e48321259bec) +++ jobs/jobs.h (date 1527996954000) @@ -28,6 +28,8 @@ #include "net/net-msg.h" #include "net/net-timers.h" +#include "common/randr_compat.h" + #define __joblocked #define __jobref Index: common/server-functions.c IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- common/server-functions.c (revision cdd348294d86e74442bb29bd6767e48321259bec) +++ common/server-functions.c (date 1527998325000) @@ -35,7 +35,9 @@ #include <arpa/inet.h> #include <assert.h> #include <errno.h> +#ifdef __GLIBC__ #include <execinfo.h> +#endif #include <fcntl.h> #include <getopt.h> #include <grp.h> @@ -168,6 +170,7 @@ } void print_backtrace (void) { +#ifdef __GLIBC__ void *buffer[64]; int nptrs = backtrace (buffer, 64); kwrite (2, "\n------- Stack Backtrace -------\n", 33); @@ -178,6 +181,7 @@ kwrite (2, s, strlen (s)); kwrite (2, "\n", 1); } +#endif } pthread_t debug_main_pthread_id; Index: common/randr_compat.h IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- common/randr_compat.h (date 1527998264000) +++ common/randr_compat.h (date 1527998264000) @@ -0,0 +1,72 @@ +/* + The GNU C Library is free software. See the file COPYING.LIB for copying + conditions, and LICENSES for notices about a few contributions that require + these additional notices to be distributed. License copyright years may be + listed using range notation, eg, 2000-2011, indicating that every year in + the range, inclusive, is a copyrightable year that would otherwise be listed + individually. +*/ + +#pragma once + +#include <endian.h> +#include <pthread.h> + +struct drand48_data { + unsigned short int __x[3]; /* Current state. */ + unsigned short int __old_x[3]; /* Old state. */ + unsigned short int __c; /* Additive const. in congruential formula. */ + unsigned short int __init; /* Flag for initializing. */ + unsigned long long int __a; /* Factor in congruential formula. */ +}; + +union ieee754_double +{ + double d; + + /* This is the IEEE 754 double-precision format. */ + struct + { +#if __BYTE_ORDER == __BIG_ENDIAN + unsigned int negative:1; + unsigned int exponent:11; + /* Together these comprise the mantissa. */ + unsigned int mantissa0:20; + unsigned int mantissa1:32; +#endif /* Big endian. */ +#if __BYTE_ORDER == __LITTLE_ENDIAN + /* Together these comprise the mantissa. */ + unsigned int mantissa1:32; + unsigned int mantissa0:20; + unsigned int exponent:11; + unsigned int negative:1; +#endif /* Little endian. */ + } ieee; + + /* This format makes it easier to see if a NaN is a signalling NaN. */ + struct + { +#if __BYTE_ORDER == __BIG_ENDIAN + unsigned int negative:1; + unsigned int exponent:11; + unsigned int quiet_nan:1; + /* Together these comprise the mantissa. */ + unsigned int mantissa0:19; + unsigned int mantissa1:32; +#else + /* Together these comprise the mantissa. */ + unsigned int mantissa1:32; + unsigned int mantissa0:19; + unsigned int quiet_nan:1; + unsigned int exponent:11; + unsigned int negative:1; +#endif + } ieee_nan; +}; + +#define IEEE754_DOUBLE_BIAS 0x3ff /* Added to exponent. */ + +int drand48_r (struct drand48_data *buffer, double *result); +int lrand48_r (struct drand48_data *buffer, long int *result); +int mrand48_r (struct drand48_data *buffer, long int *result); +int srand48_r (long int seedval, struct drand48_data *buffer); \ No newline at end of file Index: Makefile IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- Makefile (revision cdd348294d86e74442bb29bd6767e48321259bec) +++ Makefile (date 1527998107000) @@ -40,6 +40,7 @@ DEPENDENCE_NORM := $(subst ${OBJ}/,${DEP}/,$(patsubst %.o,%.d,${OBJECTS})) LIB_OBJS_NORMAL := \ + ${OBJ}/common/randr_compat.o \ ${OBJ}/common/crc32c.o \ ${OBJ}/common/pid.o \ ${OBJ}/common/sha1.o \ Index: common/randr_compat.c IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- common/randr_compat.c (date 1527998213000) +++ common/randr_compat.c (date 1527998213000) @@ -0,0 +1,120 @@ +/* + The GNU C Library is free software. See the file COPYING.LIB for copying + conditions, and LICENSES for notices about a few contributions that require + these additional notices to be distributed. License copyright years may be + listed using range notation, eg, 2000-2011, indicating that every year in + the range, inclusive, is a copyrightable year that would otherwise be listed + individually. +*/ + +#include <stddef.h> +#include "common/randr_compat.h" + +int __drand48_iterate (unsigned short int xsubi[3], struct drand48_data *buffer) { + uint64_t X; + uint64_t result; + + /* Initialize buffer, if not yet done. */ + if (!buffer->__init == 0) + { + buffer->__a = 0x5deece66dull; + buffer->__c = 0xb; + buffer->__init = 1; + } + + /* Do the real work. We choose a data type which contains at least + 48 bits. Because we compute the modulus it does not care how + many bits really are computed. */ + + X = (uint64_t) xsubi[2] << 32 | (uint32_t) xsubi[1] << 16 | xsubi[0]; + + result = X * buffer->__a + buffer->__c; + + xsubi[0] = result & 0xffff; + xsubi[1] = (result >> 16) & 0xffff; + xsubi[2] = (result >> 32) & 0xffff; + + return 0; +} + +int __erand48_r (unsigned short int xsubi[3], struct drand48_data *buffer, double *result) { + union ieee754_double temp; + + /* Compute next state. */ + if (__drand48_iterate (xsubi, buffer) < 0) + return -1; + + /* Construct a positive double with the 48 random bits distributed over + its fractional part so the resulting FP number is [0.0,1.0). */ + + temp.ieee.negative = 0; + temp.ieee.exponent = IEEE754_DOUBLE_BIAS; + temp.ieee.mantissa0 = (xsubi[2] << 4) | (xsubi[1] >> 12); + temp.ieee.mantissa1 = ((xsubi[1] & 0xfff) << 20) | (xsubi[0] << 4); + + /* Please note the lower 4 bits of mantissa1 are always 0. */ + *result = temp.d - 1.0; + + return 0; +} + +int __nrand48_r (unsigned short int xsubi[3], struct drand48_data *buffer, long int *result) { + /* Compute next state. */ + if (__drand48_iterate (xsubi, buffer) < 0) + return -1; + + /* Store the result. */ + if (sizeof (unsigned short int) == 2) + *result = xsubi[2] << 15 | xsubi[1] >> 1; + else + *result = xsubi[2] >> 1; + + return 0; +} + +int __jrand48_r (unsigned short int xsubi[3], struct drand48_data *buffer, long int *result) { + /* Compute next state. */ + if (__drand48_iterate (xsubi, buffer) < 0) + return -1; + + /* Store the result. */ + *result = (int32_t) ((xsubi[2] << 16) | xsubi[1]); + + return 0; +} + +int drand48_r (struct drand48_data *buffer, double *result) { + return __erand48_r (buffer->__x, buffer, result); +} + +int lrand48_r (struct drand48_data *buffer, long int *result) { + /* Be generous for the arguments, detect some errors. */ + if (buffer == NULL) + return -1; + + return __nrand48_r (buffer->__x, buffer, result); +} + +int mrand48_r (struct drand48_data *buffer, long int *result) { + /* Be generous for the arguments, detect some errors. */ + if (buffer == NULL) + return -1; + + return __jrand48_r (buffer->__x, buffer, result); +} + +int srand48_r (long int seedval, struct drand48_data *buffer) { + /* The standards say we only have 32 bits. */ + if (sizeof (long int) > 4) + seedval &= 0xffffffffl; + + buffer->__x[2] = seedval >> 16; + buffer->__x[1] = seedval & 0xffffl; + buffer->__x[0] = 0x330e; + + buffer->__a = 0x5deece66dull; + buffer->__c = 0xb; + buffer->__init = 1; + + return 0; +} \ No newline at end of file 

ضعه في مجلد ./patches وقم بتعديل ملف Dockerfile قليلاً لتطبيق التصحيح على الطاير:

 FROM alpine:3.6 COPY ./patches /mtproxy/patches RUN apk add --no-cache --virtual .build-deps \ git make gcc musl-dev linux-headers openssl-dev \ && git clone --single-branch --depth 1 https://github.com/TelegramMessenger/MTProxy.git /mtproxy/sources \ && cd /mtproxy/sources \ && patch -p0 -i /mtproxy/patches/randr_compat.patch \ && make -j$(getconf _NPROCESSORS_ONLN) \ && cp /mtproxy/sources/objs/bin/mtproto-proxy /mtproxy/ \ && rm -rf /mtproxy/{sources,patches} \ && apk add --no-cache --virtual .rundeps libcrypto1.0 \ && apk del .build-deps 

الآن تم إطلاق ثنائي mtproto-proxy المجمّع على الأقل ، ويمكننا المضي قدمًا.

إزالة


حان الوقت لتحويل run.sh الأصلي إلى docker-entrypoint.sh. في رأيي ، هذا أمر منطقي عندما يدخل "الربط الإلزامي" إلى ENTRYPOINT (يمكن دائمًا تحميله من الخارج) ، وتناسب الحجج الخاصة بتشغيل التطبيق المحمي بالحد الأقصى في CMD (+ متغيرات البيئة كدراسة).

يمكننا تثبيت bash و grep الكامل في صورتنا الألبية (سأشرح لاحقًا) لتجنب الصداع واستخدام الكود الأصلي كما هو ، لكنه سيضخم صورنا المصغرة للعار ، لذلك سننمو حقيقيًا ، والدته ، بونساي.

لنبدأ مع shebang ، استبدل #!/bin/bash بـ #!/bin/sh . افتراضي الرماد الألبي قادر على هضم كل بنية bash "كما هي" تقريبًا ، لكننا ما زلنا نواجه مشكلة واحدة - لأسباب غير معروفة ، رفض قبول قوس في أحد الشروط ، لذلك سنقوم بتوسيعه عن طريق عكس منطق المقارنة:



نحن الآن في انتظار مواجهة مع grep ، والتي تختلف في تسليم مشغول boxbox قليلاً عن المعتاد (وبالمناسبة ، أبطأ كثيرًا ، ضع في اعتبارك في مشاريعك). أولاً ، إنه لا يفهم التعبير {,15} ، وسيتعين عليه تحديد {0,15} بشكل صريح. ثانيًا ، لا يدعم علامة -P (نمط perl) ، ولكنه يهضم التعبير بهدوء عند تمكين الموسع (-E).

في تبعياتنا ، لا يزال هناك تجعيد فقط (ليس هناك جدوى من استبداله بـ wget من busybox) و libcrypto (هذا يكفي ، opensl غير مطلوب على الإطلاق في هذا التجميع).

ظهر بناء جميل متعدد المراحل في Docker قبل بضع سنوات ، وهو مثالي ، على سبيل المثال ، لتطبيقات Go أو في المواقف التي يكون فيها التجميع معقدًا ويسهل نقل العناصر من صورة إلى صورة بدلاً من إجراء التنظيف النهائي. سوف نستخدمه لزرع بونساي لدينا ، وهذا سيوفر القليل.

 FROM alpine:3.6 #         ( ) RUN apk add --no-cache --virtual .build-deps \ # ... ,    && make -j$(getconf _NPROCESSORS_ONLN) FROM alpine:3.6 #   ,  ,   WORKDIR /mtproxy COPY --from=0 /mtproxy/sources/objs/bin/mtproto-proxy . #          #          

يجب أن يكون بونساي بونساي - تخلص من تثبيت libcrypto. عند البناء ، كنا بحاجة إلى ملفات الرأس من حزمة openssl-dev ، والتي ستسحب في التبعيات libcrypto وسيتم توجيه ملفنا التنفيذي نحو استخدام libcrypto.so.1.0.0. ولكن هذا هو التبعية الوحيدة ، بالإضافة إلى ذلك ، يتم تثبيته مسبقًا في Alpine (في الإصدار 3.6 هو libcrypto.so.41 ، 3.7 - libcrypto.so.42 ، إنه في / lib /). لقد وبخوني الآن ، هذه ليست الطريقة الأكثر موثوقية ، لكنها تستحق ذلك وما زلنا نضيف رابطًا رمزيًا إلى الإصدار الحالي (إذا كان لديك طريقة أفضل ، سأقبل بكل سرور العلاقات العامة).

اللمسات النهائية والنتيجة:

مركز عامل الميناء
جيثب

سأكون ممتنا لأية نصيحة ومساهمات.

Source: https://habr.com/ru/post/ar413881/


All Articles