مشكلتنا مع التبعيات

لعقود من الزمن ، تمت مناقشة إعادة استخدام البرنامج في كثير من الأحيان أكثر مما كانت عليه بالفعل. الوضع اليوم هو عكس ذلك: المطورين إعادة استخدام برامج الآخرين كل يوم في شكل تبعية البرمجيات ، والمشكلة نفسها لا تزال غير مستكشفة تقريبا.

تشمل تجربتي الخاصة عقدًا من العمل مع مستودع Google الداخلي ، حيث يتم تعيين التبعيات كمفهوم للأولوية ، فضلاً عن تطوير نظام التبعية للغة برمجة Go .

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

ما هو الإدمان؟


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

يعد قبول التبعيات الخارجية من الممارسات القديمة: يقوم معظم المبرمجين بتنزيل وتثبيت المكتبة الضرورية ، سواء أكانت PCRE أو zlib من C أو Boost أو Qt من C ++ أو JodaTime أو Junit من Java. تحتوي هذه الحزم على رمز تصحيح عالي الجودة يتطلب خبرة كبيرة لإنشاءه. إذا كان البرنامج يحتاج إلى وظائف مثل هذه الحزمة ، فمن الأسهل بكثير تنزيل وتثبيت وتحديث الحزمة يدويًا بدلاً من تطوير هذه الوظيفة من البداية. ولكن التكاليف الأولية الكبيرة تعني أن إعادة الاستخدام اليدوي باهظة الثمن: من السهل كتابة الحزم الصغيرة.

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

على سبيل المثال ، يوفر مدير التبعية Node.js يسمى NPM الوصول إلى أكثر من 750،000 الحزم. إحداها ، escape-string-regexp ، تحتوي على وظيفة واحدة تفلت من عوامل التعبير العادية من بيانات الإدخال. كل التنفيذ:

 var matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g; module.exports = function (str) { if (typeof str !== 'string') { throw new TypeError('Expected a string'); } return str.replace(matchOperatorsRe, '\\$&'); }; 

قبل ظهور مديري التبعية ، كان من المستحيل تخيل نشر مكتبة مؤلفة من ثمانية أسطر: الكثير من النفقات العامة وفائدة ضئيلة للغاية. لكن NPM خفضت الحمل إلى الصفر تقريبًا ، مما أدى إلى إمكانية إعادة استخدام وظائف تافهة وإعادة استخدامها. في نهاية كانون الثاني (يناير) 2019 ، تم اعتماد تبعية escape-string-regexp حوالي ألف حزمة NPM أخرى ، ناهيك عن جميع الحزم التي يكتبها المطورون لاستخدامهم الخاص ولا ينشرون في المجال العام.

الآن ظهرت مديري التبعية لكل لغة برمجة تقريبًا. تحتوي كل من Maven Central (Java) و Nuget (.NET) و Packagist (PHP) و PyPI (Python) و RubyGems (Ruby) على أكثر من 100000 حزمة. يعد ظهور إعادة الاستخدام الواسعة للحزم الصغيرة أحد أكبر التغييرات في تطوير البرمجيات على مدار العقدين الماضيين. وإذا لم نكن أكثر حرصًا ، فسيؤدي ذلك إلى مشاكل خطيرة.

ما يمكن أن يحدث الخطأ؟


في سياق هذه المناقشة ، يتم تنزيل الكود من الإنترنت. تؤدي إضافة تبعية إلى تكليف عمل تطوير هذا الرمز - التصميم والكتابة والاختبار وتصحيح الأخطاء والدعم - لشخص آخر على الإنترنت لا تعرفه عادة. باستخدام هذا الرمز ، فإنك تعرض البرنامج الخاص بك لآثار جميع الأعطال وأوجه القصور في التبعية. يعتمد تنفيذ البرنامج الآن حرفيًا على رمز شخص غريب من الإنترنت. بعبارة أخرى ، يبدو كل شيء غير آمن للغاية. لماذا أي شخص حتى يوافق على هذا؟

نحن متفقون ، لأنه أمر سهل ، لأن كل شيء يبدو ناجحًا ، لأن كل شخص آخر يفعل ذلك أيضًا ، والأهم من ذلك ، لأنه يبدو أنه استمرار طبيعي لممارسة راسخة منذ قرون. لكن هناك فرق مهم نتجاهله.

منذ عقود ، وثق معظم المطورين الآخرين في كتابة البرامج التي كانوا يعتمدون عليها ، مثل أنظمة التشغيل والمجمعين. تم شراء هذا البرنامج من مصادر معروفة ، غالبًا مع وجود نوع من اتفاقية الدعم. لا يزال هناك مجال للأخطاء أو التدمير الكامل . لكننا على الأقل كنا نعرف من الذي نتعامل معه ، وكقاعدة عامة ، يمكننا استخدام تدابير التأثير التجاري أو القانوني.

حلت ظاهرة البرمجيات مفتوحة المصدر ، والتي يتم توزيعها مجانًا عبر الإنترنت ، محل الممارسة القديمة المتمثلة في شراء البرامج. عندما كانت إعادة الاستخدام لا تزال صعبة ، أدخلت عدد قليل من المشاريع مثل هذه التبعيات. على الرغم من أن تراخيصهم عادة ما تتنازل عن أي "ضمانات ذات قيمة تجارية ومدى ملاءمتها لغرض معين" ، إلا أن المشاريع تبني سمعة طيبة. أخذ المستخدمون هذه السمعة في الاعتبار إلى حد كبير عند اتخاذ قراراتهم. بدلاً من التدخلات التجارية والقانونية ، جاء دعم السمعة. لا تزال العديد من الحزم الشائعة في تلك الحقبة تتمتع بسمعة طيبة: على سبيل المثال ، BLAS (نُشر في عام 1979) و Netlib (1987) و libjpeg (1991) و LAPACK (1992) و HP STL (1994) و zlib (1995).

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

يمكن اعتبار تكلفة إجراء الإدمان السيئ على أنها مجموع كل النتائج السيئة المحتملة في سلسلة من سعر كل نتيجة سيئة مضروبة في احتمالها (خطر).


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

بغض النظر عن السعر المتوقع ، هناك بعض الطرق لتقييم وتقليل مخاطر إضافة التبعيات. من المحتمل أن يتم تحسين مديري الحزم لتقليل هذه المخاطر ، بينما ركزوا حتى الآن على تقليل تكلفة التنزيل والتثبيت.

الاختيار التبعية


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

يمكن أن يوفر الفحص الأساسي فكرة عن احتمال حدوث مشاكل عند محاولة استخدام هذا الرمز. إذا تم العثور على مشاكل بسيطة أثناء التفتيش ، يمكنك اتخاذ تدابير للقضاء عليها. إذا كشف الفحص عن مشكلات خطيرة ، فقد يكون من الأفضل عدم استخدام الحزمة: قد تجد حزمة أكثر ملاءمة ، أو ربما تحتاج إلى تطويرها بنفسك. تذكر أن الحزم مفتوحة المصدر يتم نشرها من قبل المؤلفين على أمل أن تكون مفيدة ، ولكن دون ضمان سهولة الاستخدام أو الدعم. في حالة فشل الإنتاج ، فإن الأمر متروك لك لتصحيح الخطأ. كما حذر أول رخصة جنو العمومية ، "كل المخاطر المرتبطة بجودة وأداء البرنامج تقع على عاتقك. إذا تبين أن البرنامج معيب ، فإنك تتحمل تكاليف جميع أعمال الصيانة أو الإصلاح أو التصحيح اللازمة. "

بعد ذلك ، نوضح بعض الاعتبارات لفحص الحزمة وتحديد ما إذا كنت ستعتمد عليها.

تصميم


هل وثائق الحزمة واضحة؟ هل لدى API تصميم واضح؟ إذا كان بإمكان المؤلفين شرح واجهة برمجة التطبيقات والتصميم لأي شخص بشكل جيد ، فإن هذا يزيد من احتمال قيامهم أيضًا بتفسير تطبيق الكمبيوتر في التعليمات البرمجية المصدر جيدًا. كتابة التعليمات البرمجية لواجهة برمجة التطبيقات الواضحة والمصممة بشكل جيد أبسط وأسرع وربما أقل عرضة للخطأ. هل قام المؤلفون بتوثيق ما يتوقعونه من كود العميل ليكون متوافقًا مع التحديثات المستقبلية؟ (تتضمن الأمثلة مستندات توافق C ++ و Go ).

جودة الرمز


هل الرمز مكتوب بشكل جيد؟ اقرأ بعض المقتطفات. هل يبدو أن المؤلفين حريصون وضميرون ومتسقون؟ هل تبدو مثل الكود الذي تريد تصحيحه؟ قد تضطر إلى القيام بذلك.

طور طرقك المنهجية للتحقق من جودة الكود. هناك شيء بسيط ، مثل التحويل البرمجي في C أو C ++ مع تحذيرات المحول البرمجي المهمة قيد التشغيل (على سبيل المثال ، -Wall ) ، يمكن أن يعطي فكرة عن مدى جدية المطورين الذين عملوا على تجنب سلوكيات غير محددة مختلفة. تستخدم اللغات الحديثة ، مثل Go و Rust و Swift ، الكلمة الأساسية unsafe للإشارة إلى الرمز الذي ينتهك نظام الكتابة ؛ انظر كم رمز غير آمن هناك. الأدوات الدلالية الأكثر تقدمًا مثل Infer أو SpotBugs مفيدة أيضًا. تعتبر Linters أقل فائدة: يجب عليك تجاهل النصائح القياسية حول مواضيع مثل نمط الأقواس والتركيز على المشكلات الدلالية.

لا تنس طرق التطوير التي قد لا تكون على دراية بها. على سبيل المثال ، تأتي مكتبة SQLite كملف واحد برمز 200000 ورأس 11000 سطر - نتيجة لدمج ملفات متعددة. إن حجم هذه الملفات في حد ذاته يثير علامة حمراء على الفور ، لكن البحث الأكثر شمولاً سيؤدي إلى الكود المصدري الفعلي للتطوير: شجرة ملفات تقليدية بها أكثر من مئة ملف من ملفات C ، واختبارات وبرامج نصية للدعم. لقد اتضح أن توزيع الملفات الفردية مبني تلقائيًا من المصادر الأصلية: هذا سهل للمستخدمين النهائيين ، وخاصة أولئك الذين ليس لديهم مديرو تبعية. (رمز المترجمة يعمل أيضا بشكل أسرع لأن المترجم يرى المزيد من خيارات التحسين).

تجريب


هل هناك أي اختبارات في الكود؟ يمكنك السيطرة عليها؟ هل يمرون؟ تثبت الاختبارات أن الوظيفة الرئيسية للرمز صحيحة ، وتشير إلى أن المطور يحاول بجدية الاحتفاظ بها. على سبيل المثال ، تحتوي شجرة تطوير SQLite على مجموعة اختبار مفصلة بشكل لا يصدق مع أكثر من 30000 حالة اختبار فردية. هناك وثائق للمطورين تشرح استراتيجية الاختبار. من ناحية أخرى ، إذا كانت هناك اختبارات قليلة أو معدومة على الإطلاق ، أو إذا فشلت الاختبارات ، فهذا علامة حمراء خطيرة: من المحتمل أن تؤدي التغييرات المستقبلية في الحزمة إلى انحدارات يمكن اكتشافها بسهولة. إذا كنت تصر على الاختبارات في الكود (يمين؟) ، يجب عليك تقديم اختبارات للكود الذي تنقله للآخرين.

بافتراض وجود اختبارات وتشغيلها وتمريرها ، يمكنك جمع معلومات إضافية عن طريق تشغيل أدوات لتحليل تغطية الشفرة واكتشاف ظروف السباق والتحقق من تخصيص الذاكرة واكتشاف تسرب الذاكرة.

تصحيح الأخطاء


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

دعم


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

من ناحية أخرى ، فإن بعض الكود "مثالي" حقًا. على سبيل المثال ، قد لا تحتاج أبدًا إلى تغيير escape-string-regexp من NPM مرة أخرى.

استخدام


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

على سبيل المثال ، تستخدم المكتبات مثل PCRE أو Boost أو JUnit على نطاق واسع بشكل لا يصدق. هذا يجعل الأمر أكثر ترجيحًا - على الرغم من أنه لا يضمن بالتأكيد - أن الأخطاء التي قد تكون واجهتها قد تم إصلاحها بالفعل لأن الآخرين واجهوها قبل ذلك.

السلامة


هل ستعمل هذه الحزمة مع إدخال غير آمن؟ إذا كان الأمر كذلك ، فما مدى مقاومة البيانات الضارة؟ هل لديه الأخطاء المذكورة في قاعدة بيانات الضعف الوطنية (NVD) ؟

على سبيل المثال ، عندما بدأت أنا وجيف دين في عام 2006 العمل في Google Code Search ( grep لقواعد الكود العام) ، بدا أن مكتبة التعبير المعتادة الشعبية PCRE هي الاختيار الواضح. ومع ذلك ، في محادثة مع فريق أمان Google ، علمنا أن PCRE لديه تاريخ طويل من المشاكل ، مثل الفائض المؤقت ، وخاصة في المحلل اللغوي. نحن أنفسنا كنا مقتنعين بذلك من خلال البحث عن PCRE في NVD. هذا الاكتشاف لم يدفعنا على الفور إلى التخلي عن PCRE ، ولكنه جعلنا نفكر بمزيد من الدقة في الاختبار والعزل.

الترخيص


هل الكود مرخص بشكل صحيح؟ هل لديه حتى رخصة؟ هل الترخيص مقبول لمشروعك أو شركتك؟ جزء مذهل من مشاريع جيثب لا تملك رخصة واضحة. قد يضع مشروعك أو شركتك قيودًا إضافية على تراخيص التبعية. على سبيل المثال ، تحظر Google استخدام الشفرة بموجب تراخيص مثل AGPL (ضيقة جدًا) واكتب WTFPL (غامضة جدًا).

اعتمادا على


هل هذه الحزمة لها التبعيات الخاصة بها؟ أوجه القصور في التبعيات غير المباشرة ضارة بنفس القدر من العيوب في التبعيات المباشرة. يمكن لمديري الحزم سرد جميع التبعيات العابرة لحزمة معينة ، وينبغي التحقق من كل واحدة منها كما هو موضح في هذا القسم. حزمة مع العديد من التبعيات تتطلب الكثير من العمل.

لم ينظر العديد من المطورين أبدًا إلى القائمة الكاملة للتبعيات متعدية من التعليمات البرمجية الخاصة بهم ولا يعرفون ما الذي يعتمدون عليه. على سبيل المثال ، في مارس 2016 ، وجد مجتمع مستخدمي NPM أن العديد من المشاريع الشائعة - بما في ذلك Babel و Ember و React - تعتمد بشكل غير مباشر على حزمة صغيرة تسمى left-pad من وظيفة ذات 8 أسطر. اكتشفوا ذلك عندما قام مؤلف left-pad بإزالة الحزمة من NPM ، متعمدًا كسر معظم مجموعات مستخدمي Node.js. والجزء left-pad بالكاد استثنائي في هذا الصدد. على سبيل المثال ، تعتمد 30٪ من الحزم التي يبلغ عددها 750،000 في NPM - على الأقل بشكل غير مباشر - على escape-string-regexp . عند تكييف ملاحظة Leslie Lamport للأنظمة الموزعة ، يقوم مدير الحزم بسهولة بإنشاء موقف حيث يمكن أن يؤدي فشل الحزمة ، الذي لم تعرف وجوده حتى ، إلى جعل الشفرة الخاصة بك غير قابلة للاستخدام.

اختبار الإدمان


يجب أن تتضمن عملية التحقق إجراء اختبارات الحزمة الخاصة بك. إذا نجحت الحزمة في الاختبار وقررت جعل مشروعك يعتمد عليه ، فستكون الخطوة التالية هي كتابة اختبارات جديدة تركز على وظيفة التطبيق الخاص بك على وجه التحديد. تبدأ هذه الاختبارات غالبًا كبرامج قصيرة قائمة بذاتها للتأكد من أنه يمكنك فهم حزمة واجهة برمجة التطبيقات (API) الخاصة بالحزمة وبأنها تفعل ما تفكر فيه (إذا لم تستطع فهمه أو لم تفعل ما تحتاجه ، فتوقف فورًا!). ثم يستحق الأمر بذل جهد إضافي لتحويل هذه البرامج إلى اختبارات آلية يتم تشغيلها مع إصدارات جديدة من الحزمة. إذا وجدت خطأ ولديك إصلاحًا محتملًا ، يمكنك بسهولة إعادة تشغيل هذه الاختبارات لمشروع معين والتأكد من أن الإصلاح لم يكسر أي شيء آخر.

يجب إيلاء اهتمام خاص لمجالات المشكلات المحددة خلال مراجعة خط الأساس. بالنسبة إلى Code Search ، من التجربة السابقة ، عرفنا أن PCRE أحيانًا يستغرق وقتًا طويلاً لتنفيذ تعبيرات عادية معينة. كانت خطتنا الأولية هي إنشاء تجمعات مؤشرات منفصلة للتعبيرات العادية "البسيطة" و "المعقدة". كان أحد الاختبارات الأولى معيارًا للمقارنة بين pcregrep والعديد من تطبيقات grep الأخرى. عندما وجدنا أن pcregrep كان أبطأ بنسبة 70 مرة من أسرع grep لحالة اختبار أساسية واحدة ، بدأنا في إعادة التفكير في خطتنا لاستخدام PCRE. على الرغم من حقيقة أننا تخلينا تمامًا عن PCRE ، يبقى هذا الاختبار في قاعدة الكود الخاصة بنا اليوم.

تجريد التبعية


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

, . , API , , API, . , , . , , , , . , , . , .

Code Search Regexp , Code Search, . PCRE, . PCRE . , , , .


, , . , Google Chrome — . Chrome 2008 , ( ) , . . Code Search, PCRE , , PCRE . , gVisor . .

- . , . , C C++, , , Java JNI, Go, Rust Swift, unsafe. , JavaScript, , . 2018 . , npm event-stream ( API JavaScript) , . - Copay, , . .


, .

, PCRE, Google Code Search « PCRE » « PCRE, », « , PCRE», « , ». , , : RE2 .

, , (, ). , . ., . Go : « , ».


: « , ». ; — , ? . -, . : — , . -, . , , , .

, Equifax 2017 , . 7 Apache Struts . 8 Equifax US-CERT Apache Struts. Equifax 9 15 ; -, . 13 , Equifax . Apache Struts Equifax, 148 . , 29 Equifax 4 . Equifax, CIO CSO , .

Equifax , , , . Go , , . Go , , .

, , . , , , , , , , . , , , .

, , , , , . . , , ( ), , . , .

, . , .

, , .

. Equifax , (, ) Apache Struts 10 , , . whoami .


. - .

-, , . , . , , . event-stream , 3.3.5. 3.3.6 , ( ).

: , . . event-stream flatMap-stream , event-stream .

. Google Sawzall — JIT- — , JIT Sawzall, () PostScript, Python JavaScript. , - Sawzall, , Google . Go .

— . , . , ? ? , .

. , Apache Struts 2016, 2017 2018 . , , , .

استنتاج


, , , : . , , . , , -.

, , , . , , , . . : « , ». - .

Copay Equifax , . . .

  1. . , , , . , .
  2. . , , . , , . , , , .
  3. . . . , . , , . , , API. .

هناك الكثير من البرامج الجيدة. دعونا نعمل معًا ونكتشف كيفية استخدامها بأمان.

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


All Articles