← До списку уроків
Заняття 08 з 13Перевіряємо бота на міцність
курс 0% виконано0%
Заняття 08 · місяць 2

Перевіряємо бота на міцність

90 хвилин

Знайти слабкі місця свого бота й закрити їх: погані повідомлення його не валять, кожне оновлення лишає рядок у журналі, база копіюється щодня і з копії вже відновлювались, а служба піднімається після аварії. Чужого бота перевіряти лише з дозволу власника.

Мій сайт
http://91.219.61.4/u/<логін>/
Мій кабінет
http://91.219.61.4/k/<логін>/
Вхід на сервер
ssh <логін>@91.219.61.4

Замість <логін> підставляєш свій логін виду bohdan.g9: ім’я, крапка, номер групи. Кутові дужки не пишуться. Кабінет — сторінка, де проти кожного критерію стоїть відмітка, зарахований він чи ні. У прикладах на цій сторінці твій логін — bohdan.g9, а логін однокласника, сервіс якого ти перевіряєш, — marta.g9. Обидва читай як справжні логіни й виправляй у кожній команді, яку копіюєш.

Що ти зробиш сьогодні

Досі ти перевіряв, що бот працює: відповідає на /start, записує людей у базу, пускає до списку лише тебе. Сьогодні перевіриш, що він не ламається, коли все йде не за планом: хтось надсилає повідомлення на п’ять тисяч літер, одні емодзі, стікер замість тексту чи порожнє оновлення, процес служби раптом убивають, а файл бази псується. Так роблять професійні розробники (ті, хто пише програми за гроші) перед тим, як показати програму людям, і називають це перевіркою на міцність.

До кінця заняття в бота з’явиться:

Показати можна буде: вивід stress.py — «добре» на всі погані повідомлення; рядки app.log, які з’являються наживо; копії в ~/backups і вивід restore.py; бота, що сам піднявся після kill -9; BUGS.md на GitHub.

Сьогодні ми захищаємося, а не нападаємоПеревіряєш лише свого бота. Бота однокласника — тільки з його дозволу, тільки повідомленнями в Telegram — і так, щоб нічого не зіпсувати. Заходити в чужий акаунт, відкривати чужі файли, підбирати паролі, засипати бота повідомленнями не можна навіть «для перевірки». Знайдене кажеш власникові, а не використовуєш. Чому так — у першому розділі теорії.

Теорія

1. Перевірка на міцність: своє — завжди, чуже — лише за згодою

Надійний бот не «втрачає голову», коли все пішло не так: на погані дані чемно відмовляє, після аварії сам піднімається, а втрачену базу можна повернути з копії. Перевірити це можна одним способом — навмисно створити погану ситуацію й подивитися, що вийде. Вміння ламати своє — частина професії розробника.

Ті самі дії з чужим сервісом без дозволу — уже не перевірка, а втручання: межу визначає не те, що ти робиш, а те, чи дозволив власник. Фахівців, які шукають слабкі місця за згодою власника, називають «білими хакерами», і компанії навіть платять їм за знахідки. Без згоди те саме — порушення. У законі це звучить як «несанкціоноване втручання в роботу комп’ютерних систем» (стаття 361 Кримінального кодексу України) і вважається злочином. Простими словами: заходити туди, куди тебе не пускали, не можна. А на нашому сервері діє ще й правило курсу: у чужий акаунт не заходять, чужих файлів не відкривають, паролів не підбирають.

Тому сьогоднішня перевірка однокласника має чотири умови:

  1. Згода. Він сам сказав «так» і назвав нік свого бота.
  2. Лише ззовні. Тільки повідомлення його боту в Telegram — ніякого SSH і ніяких файлів.
  3. Нічого не зіпсувати. Кожен запит складаєш так, щоб він нічого не змінив, навіть якщо захисту немає. Замок на видаленні перевіряють командою /del 999999 до запису, якого не існує: працює замок — прийде відмова «лише для власника», не працює — «такого контакту немає», і все одно нічого не видалено. Це як смикнути ручку дверей, а не виламувати їх.
  4. Знахідку — власникові. Особисто, а не в загальний чат: поки дірку не закрито, кожен, хто про неї знає, може нею скористатися. Таке правило називають відповідальним розкриттям.

Як користуватися ШІ-помічникомМожна попросити ШІ пояснити незнайомий рядок помилки або підказати, які ще погані дані спробувати на своєму сервісі. Не вставляй у чат .env, BOT_TOKEN і рядки з бази — там секрети й імена людей. Не запускай на спільному сервері команд, яких не розумієш. Виправлення пиши сам, а в BUGS.md пояснюй своїми словами: на занятті тебе попросять розповісти, що саме було не так.

Марта перевіряла бота Петра за його згодою й помітила: команда /users показує імена всіх, хто писав боту, кожному, хто її надішле, — Петро не доробив заняття 07. Що їй робити?

Red Team і Blue Team: війна заради безпеки

Великі компанії, банки й армії перевіряють свою безпеку не лише списками правил, а навчальними боями. Беруть двох «суперників» усередині одного колективу:

КомандаХто цеЩо робитьЧим виграє
Red Team («червоні»)нападникишукають слабкі місця так, як це зробив би справжній зловмисник, але з дозволу й за правиламизнаходить дірку й показує, що вона справжня
Blue Team («сині»)захисникистежать за журналами, помічають напад, закривають дірки й відновлюють роботувчасно бачить напад і не дає нічого вкрасти чи зламати

Назви прийшли з військових навчань: «червоні» грають за супротивника, «сині» — за своїх. Коли бій скінчився, обидві команди сідають разом і розбирають, що сталося: де нападники пройшли, де захисники помітили, а де проґавили. Таке спільне розбирання інколи називають Purple Team («фіолетові» — суміш червоного й синього). Саме тут народжується користь: гра не заради перемоги, а заради того, щоб після неї захист був міцнішим.

Чому гра, а не справжній нападЗахист, який жодного разу не перевіряли нападом, — це лише надія. Справжній злочинець не скаже, що пройшов. А товариш по команді скаже й допоможе закрити дірку. Тому нападати дозволено лише в грі, за згодою господаря, з правилами й словом «стоп». Без цього те саме було б правопорушенням.

Правила нашого бою

Червона команда першою знайшла, що в боті немає замка на команді /users. Що найкраще для бою?

Хто виграє від навчального бою Red Team проти Blue Team?

Бот пускає до /users тих, чиє ім’я в Telegram — «Власник». Що не так?

Red Team знайшла у старому коміті справжній токен бота, а Blue Team видалила файл новим комітом. Витік закрито?

Blue Team змінила службу бота, але не виконала daemon-reload і не перезапустила її. Вона каже: «Закрито». Що відповіси?

2. Погані повідомлення: бот не падає і не мовчить

Бот — це програма, яка приймає дані від незнайомих людей. А люди (і програми, які за них пишуть) надсилають усяке: порожнє повідомлення, стікер замість тексту, /ask без питання, повідомлення на п’ять тисяч літер, одні емодзі, команду зі сміттям після неї, відредаговане повідомлення, натискання кнопки. Це звичайні ситуації, а не аварії. Головне правило дня: на будь-які погані дані надійний бот відповідає спокійно й лишається живим. Якщо одне дивне повідомлення зупиняє бота для всіх, автор не передбачив такого випадку, і функція впала посеред роботи.

Що буває в оновленні, окрім звичайного тексту (update — це те, що Telegram віддає боту):

Що прийшлоЯк виглядаєЩо може зламатися
звичайний текстmessage.textнічого: цей випадок ти вже пройшов
стікер, фото, голосmessage без textкод, який рахує на текст, падає на None
відредаговане повідомленняedited_message замість messageбот не знаходить message і розгублюється
натискання кнопкиcallback_queryте саме, а ще немає ні message.text, ні, можливо, chat
дуже довгий текст5000 літервідповідь «Ти написав: …» довша за 4096 — Telegram її не прийме
дивні символиемодзі, нульовий символ \x00база чи ШІ можуть відмовитися від такого тексту

Є дві межі, про які треба пам’ятати. Telegram приймає повідомлення до 4096 символів: довшу відповідь він відхилить, і людина не отримає нічого. А ще — що довший вхідний текст, то більше роботи для бази й ШІ, тож задовгі повідомлення розумно обрізати ще на вході.

Перша лінія захисту у твоєму боті вже є: цикл poll_forever() загортає обробку кожного повідомлення в try … except. Якщо логіка впаде, людина отримає «Ой, щось пішло не так», у журнал служби піде рядок помилка обробки, а бот відповідатиме іншим. Але цей захист стоїть на доставці, а не на самій обробці. Сьогодні ти додаси другий — безпосередньо навколо handle_update(), у готовому файлі robust.py. Це декоратор: рядок @robust.safe над функцією, який огортає її, як обкладинка книжку, і щоразу, коли викликають handle_update, спершу виконується обгортка:

@robust.safe
def handle_update(update):
    ...

Щит (від англ. robust — «міцний») — це обгортка навколо handle_update(). Він робить чотири речі: обрізає надто довгий текст, ловить будь-яку помилку всередині й повертає спокійну відповідь замість Traceback, не дає відповіді перевищити 4000 символів (запас до межі Telegram у 4096) і дописує рядок у журнал (розділ 4). На будь-яке повідомлення з текстом бот відповідає; на стікер чи фото може мовчати, але не падає.

Чому таке потрібно, коли є except у циклі? По-перше, перевірку на міцність можна провести без Telegram: stress.py викликає handle_update напряму, і така перевірка бачить помилку одразу, а не з лог-файлу. По-друге, журнал записується для кожного оновлення, в тому числі й для тих, що закінчилися помилкою. По-третє, захист, який стоїть у двох місцях, переживе помилку в одному з них.

Данило надіслав своєму боту стікер. У боті є рядок if text.startswith("/start"), а text береться як msg["text"]. Що станеться без щита?

3. Текст, що прикидається командою: SQL-ін’єкція

На занятті 04 ти дізнався, що значення в SQL-запит передають окремо, через знак ?. Подивімося, від чого саме це захищає. Уяви, що функцію додавання контакту написано склеюванням рядків:

execute("INSERT INTO users(name, first_seen, last_seen) VALUES('" + name + "', …)")

Поки людина вводить Оля, усе гаразд. Але для імені Оля'); DROP TABLE users; -- база отримає суцільний текст і не відрізнить, де закінчилася команда автора й почалися дані: лапка з імені закриває рядок, а далі йде нова команда — видалити таблицю. Це і є SQL-ін’єкція: текст, введений у поле, стає частиною команди до бази й змінює її зміст.

У твоєму коді (contacts.py, заняття 05) значення йдуть окремо: VALUES(NULL, ?, '', ?, ?, ?, 0)", (name, note, t, t). Текст запиту й дані потрапляють до бази різними шляхами, тому дані ніколи не стають командою. Ім’я Оля' OR '1'='1 так і запишеться в таблицю, дослівно, разом із лапками, — на практиці ти це побачиш.

Одне місце в contacts.py складає SQL f-рядком — edit(): changed(f"UPDATE users SET {field} = ? WHERE id = ?", …). Воно безпечне: у {field} потрапляє лише name або note — бот перевіряє це за списком раніше, — а самі значення й тут ідуть через ?. Правило: у текст запиту можна вставляти лише те, що написав автор коду, а все, що прийшло ззовні, — тільки через ?.

4. Журнал подій: навіщо в ньому час

Журнал (його ще називають логом) — рядки, які програма пише про свою роботу: що сталося й коли. Сьогодні твій бот почне записувати кожне оновлення у файл ~/logs/app.log одним рядком:

2026-11-04 14:03:11 chat=100200300 /start ok 2ms len=6
2026-11-04 14:03:40 chat=777000111 текст ok 1ms len=4821
2026-11-04 14:04:02 chat=100200300 message без-тексту ok 1ms len=0
2026-11-04 14:05:17 chat=100200300 /add ПОМИЛКА KeyError 3ms len=14

Дата й час, номер чату, що це було (команда, «текст» або вид оновлення), чи закінчилося добре, скільки мілісекунд бот думав і довжина тексту. Найцінніше тут — час. Запис «було помилку» без часу не каже, коли це почалося: бот міг працювати три дні, а зламатися після твоєї правки о 14:30. Знаючи час, ти порівнюєш його з git log і бачиш, яка зміна винна. А ще видно, хто й коли стукав у зачинені двері: десяток відмов поспіль о третій ночі — привід задуматися.

Чому в рядку немає тексту повідомлення? Бо те, що люди пишуть боту, — їхні персональні дані (розділ 1 заняття 07). Журнал читають, показують на екрані, копіюють у бекап, а секрет чи ім’я, що туди потрапили, повернути вже не можна. У журнал йде лише команда, довжина й номер чату. Щит робить це сам.

Журналів у тебе два, і вони про різне. ~/logs/app.log — рядки про кожне оновлення, яке дійшло до handle_update(). ~/logs/club-bot.log — журнал служби: усе, що друкує бот (print), — запуски служби, Telegram недоступний і, головне, Traceback кожної помилки. Тому, якщо о 14:05 ти шукаєш причину, дивись у app.log, що тоді приходило, а в club-bot.log — чому воно впало.

Чого немає в app.logОновлення, які не дійшли до handle_update(): бот вимкнений, Telegram недоступний, служба перезапускається. Тому «у журналі нічого немає» не завжди означає «нічого й не було».

Данило каже: «Однокласниця пише, що о 14:05 бот їй не відповів. Я відкрив ~/logs/app.log — за 14:05 там нічого. Отже, вона вигадує». Що відповісти Данилові?

5. Бекап: копія, з якої ти вже відновлювався

Уся пам’ять бота — один файл ~/data/app.db: хто писав, скільки разів, примітки. Утратити його можна безліччю способів: DELETE без WHERE, випадковий rm, зіпсований диск, помилка в коді, що затерла стовпець. Бекап — копія даних, зроблена заздалегідь, щоб після такої біди повернути все як було.

Чому б не просто cp ~/data/app.db ~/backups/? Бот пише в базу будь-якої миті, а cp копіює файл шматками: якщо посеред копіювання прийде повідомлення, у копії опиниться половина старого стану й половина нового — файл, який може й не відкритися. Тому backup.py робить копію через src.backup(dst) — вбудований у SQLite механізм, що копіює базу цілісною, навіть коли в неї саме пишуть. Ім’я копії містить дату й час (app-20261104-0330.db), а все, що старше за 14 останніх копій, видаляється, — інакше тека росла б, доки не скінчиться місце на спільному диску.

Тепер головне правило: бекап, з якого жодного разу не відновлювались, — не бекап. Буває, що скрипт місяцями копіює не той файл чи щоночі тихо падає, а помічають це в день біди. Тому відновлення пробують заздалегідь — і спершу в окрему теку. restore.py розгортає копію в ~/restore-test/app.db і показує, які там таблиці й скільки в них записів, а справжньої бази не чіпає: якщо копія погана, ти нічого не втратиш.

Робити копію руками щодня ніхто не стане, тому її робитиме таймер systemd — той самий systemd, що тримає твою службу, тільки за розкладом. Таймер — пара файлів: club-backup.service каже, що запустити (backup.py), а club-backup.timer — коли: OnCalendar=*-*-* 03:30, тобто «щодня о 03:30». Persistent=true означає: якщо о 03:30 сервер був вимкнений, копію зроблять одразу після його запуску.

І останнє: копії в ~/backups лежать на тому самому диску, що й база. Від DELETE без WHERE вони врятують, а від загибелі сервера — ні. Тому професіонали тримають ще одну копію деінде. Викладач покаже це на своєму екрані: перенесе бекап на інший сервер і відкриє його там.

Три історії про те, чому копії роблять регулярно

Ці історії вигадані, але такі випадки справді трапляються — і в школярів, і в дорослих.

Висновок простий: копія корисна, коли вона регулярна (щодня, а не «коли згадаю»), кількох поколінь (не лише остання) і перевірена (ти вже хоч раз відновився з неї).

Марко два місяці щоночі робив бекап власною командою. Коли база зламалася, він відкрив копії — усі порожні: команда копіювала не той файл. Що допомогло б помітити це на два місяці раніше?

6. Хто стукає у двері: боти й підбір паролів

Щойно в інтернеті з’являється новий сервер, до нього починають стукати автоматичні програми — не твій Telegram-бот, а чужі, які шукають слабкі місця. Їх ніхто не спрямовує на конкретний сервер: вони просто перебирають адреси підряд, і за хвилини знаходять і твій сервер. Далі така програма пробує зайти на нього через SSH, підставляючи пароль за паролем. Це називають підбором паролів (англійською — brute force, «груба сила»): без розуму, зате безперервно — день і ніч, без вихідних.

Ось справжні цифри з журналу нашого навчального сервера за тиждень з 18 по 25 жовтня 2026 року:

ЩоСкільки
Невдалих спроб увійти за тиждень81 299
Це в середньому на годинублизько 480
З яких різних адрес стукали479
З них спроб зайти під іменем root (головний користувач)понад 48 800
Імена, які автоматичні програми пробують найчастішеadmin, ubuntu, user, deploy, test
Справжніх входів за той самий тиждень30

Тобто на кожен справжній вхід припадає майже три тисячі спроб підбору. Роботи не знають, хто ти: вони пробують і в школярів, і в банків. Для них важливо лише одне — чи є на сервері людина зі слабким паролем. Якщо пароль 12345, qwerty чи ім’я з датою — його підберуть швидко. Саме тому пароль має бути довгим і випадковим, а ще краще — взагалі не потрібним (див. далі про ключі).

Журнал вдалих і невдалих входів на сервері — файл /var/log/secure; його читає лише адміністратор. Викладач покаже тобі на екрані, як порахувати такі спроби командою grep -c "Failed password" /var/log/secure.

Як сервер захищається

На сервері за тиждень 81 тисяча невдалих спроб входу. Що це означає?

7. SSH-ключі: вхід, який неможливо підібрати

SSH-ключ — це пара файлів, які створюються разом. Приватний ключ лишається в тебе на комп’ютері й нікому не показується. Публічний ключ можна давати будь-кому: ти кладеш його на сервер. Коли заходиш, сервер каже «доведи, що в тебе є пара до цього публічного ключа», і комп’ютер це доводить — а сам приватний ключ мережею не передається.

Порівняння: вхід за SSH — це пара «замок і ключ від нього». Замок (його файл називають публічним ключем) ти вішаєш на двері сервера, а ключ від замка (приватний ключ) лишається лише в тебе. Замків можна скільки завгодно повісити, а ключ від них — один, і він у тебе. Підібрати його роботу неможливо: приватний ключ — це число, у якому сотні знаків, а не слово, яке можна вгадати.

Вигоди ключа порівняно з паролем:

Важливо: приватний ключ (id_ed25519, без .pub) — як пароль: нікому не надсилай, не клади на GitHub і не вставляй у ШІ. Публічний (id_ed25519.pub) можна показувати. На справжніх серверах пароль взагалі вимикають і лишають тільки ключі. На нашому навчальному сервері пароль залишено, бо ви всі заходите вперше й помилки в налаштуванні ключа не мають закрити вам доступ.

Який із двох файлів ключа можна покласти на сервер, а який ніколи нікому не показують?

8. Двері сервера: чому в твого бота немає порту

Сервер схожий на будинок із багатьма дверима. Кожні двері — це порт, номер від 1 до 65535, на якому програма чекає з’єднань, тобто «слухає». SSH чекає на порту 22, nginx (він віддає твій сайт) — на 80. Які двері відчинені, показує команда ss -ltn: l — лише ті, що слухають, t — протокол TCP, n — номери портів замість назв.

А твій бот? Він нічого не слухає. Він сам ходить до Telegram і питає: «Є нові повідомлення?» (це і є long polling з заняття 02). Тобто бот — клієнт, який звертається за потреби, а не сервер, що постійно чекає на стук. Через це у твого бота немає власного порту, а значить, і зайвих дверей для чужих: усе, що може прийти ззовні, проходить через Telegram, який сам перевіряє, хто написав. Для безпеки це добре: менше відчинених дверей — менше що охороняти. Для сервера теж добре: програма, що не тримає порт і не чекає запитів, майже не їсть пам’яті.

Фаєрвол — програма, що пропускає або відкидає з’єднання ще до будь-яких дверей. На нашому сервері це firewalld, і ззовні він пропускає лише потрібне: SSH (22), HTTP (80), HTTPS (443, знадобиться на занятті 10). Налаштовує фаєрвол лише викладач: одна помилка в правилах відрізала б від сервера весь клас.

Ще кілька запобіжників стоять тому, що сервер спільний і на ньому мало пам’яті (близько 1,7 ГБ на всіх):

Богдан запускає ss -ltn і не бачить жодного рядка від свого бота (лише :22 і :80). Він злякався: «Бот не працює?» Що відповісти?

Слова цього уроку

СловоЩо означає
перевірка на міцністьНавмисно створити погану ситуацію — погані повідомлення, аварію, втрату бази — і подивитися, чи бот тримається
згода, відповідальне розкриттяДозвіл власника на перевірку; про знайдене кажуть власникові й дають час закрити, а не розповідають усім
оновлення (update)Одна порція даних від Telegram: повідомлення, редагування, натискання кнопки
декоратор, @robust.safeРядок над функцією, що огортає її: щось робить до і після виклику й може перехопити помилку
TracebackЗвіт Python про помилку: які функції викликалися й на якому рядку все зупинилося
4096Найбільше символів в одному повідомленні Telegram; довші він відхиляє
SQL-ін’єкціяТекст із поля, що потрапив у SQL-запит як частина команди й змінив її зміст
журнал, логРядки, які програма пише про свою роботу, — з датою й часом
бекап, пробне відновленняКопія даних, зроблена заздалегідь; розгорнути її в окрему теку й перевірити, не чіпаючи справжньої бази
таймер systemdПара файлів .timer і .service: що запускати й коли (OnCalendar); Persistent — надолужити пропущений запуск
портНомер «дверей», на яких програма чекає з’єднань; у бота-клієнта такого порту немає
клієнт і серверКлієнт сам звертається до інших (бот → Telegram); сервер постійно чекає на звернення і тримає порт
фаєрволПрограма, що пропускає або відкидає з’єднання ще до дверей сервера
SIGKILL, kill -9Найгрубіша зупинка процесу: система прибирає його з пам’яті без попередження
BUGS.mdФайл знахідок: що зробили, що сталося, чому й як закрили
підбір паролів (brute force)Програма-робот пробує паролі підряд, поки один не підійде
SSH-ключ (приватний і публічний)Пара файлів для входу: публічний кладуть на сервер, приватний нікому не показують

Практика

Виконуй кроки по черзі. $USER не підставляй — ця змінна вже містить твій логін, а <логін> разом із дужками заміни на свій, наприклад bohdan.g9. Номери записів і чисел у прикладах умовні. Без Telegram усе, крім перевірки живого бота, робиться через try_bot.py і stress.py.

1 Зайти на сервер і подивитися, з чого починаємо

ssh <логін>@91.219.61.4
cd ~/app
git status --short
git branch --show-current
systemctl --user is-active club-bot
grep -c "access.check" bot.py
ls /opt/club/kurs/08/

2 Зламати бота «поганими» повідомленнями — до правок

cp /opt/club/kurs/08/stress.py ~/app/
cd ~/app
python3 stress.py

Скрипт stress.py дає боту сімнадцять «поганих» оновлень одне за одним і про кожне друкує добре або ПАДІННЯ і причину. Справжню базу він не чіпає: працює з її копією, а сторінку статистики пише у тимчасову теку. Найімовірніше, побачиш щось таке:

ПАДІННЯ  5000 літер: відповідь 5012 символів, Telegram приймає до 4096
добре    довга /ask
добре    лише емодзі
…
ПАДІННЯ  message — не словник: AttributeError: 'str' object has no attribute 'get'
Не пережив: 2 з 17.

Причина для 5000 літер проста: бот відповідає «Ти написав: …» і повторює увесь текст. Це знахідка №1, запиши її в голові для BUGS.md. Скільки саме «ПАДІНЬ» у тебе, залежить від твого бота. Нічого не змінилося — це лише перевірка. No module named 'yaml' — запускай /opt/club/venv/bin/python3 stress.py.

3 Підключити щит robust.py

cp /opt/club/kurs/08/robust.py ~/app/
less ~/app/robust.py

Знайди в файлі: MAX_IN = 1000 і MAX_OUT = 4000 — межі; функцію describe(), що вирішує, що саме записати в журнал (тексту повідомлення в ній немає навмисно); safe() — сам декоратор: try … except Exception, обрізання відповіді й рядок write_log(…). Вийти з less — q. Тепер відкрий nano ~/app/bot.py і зроби дві вставки. Перша — разом з іншими імпортами (за абеткою):

import robust

Друга — рядок @robust.safe прямо над def handle_update(update):, без порожнього рядка між ними й з тим самим відступом (відступу немає, бо функція стоїть у лівому краї):

@robust.safe
def handle_update(update):

Збережи (Ctrl+O, Enter), вийди (Ctrl+X) і перевір файл тим самим Python, яким його запускає служба:

/opt/club/venv/bin/python3 -c "import bot" && echo готово

Має бути готово. ModuleNotFoundError: No module named 'robust' — файлу немає в ~/app. NameError: name 'robust' is not defined — пропущено import robust. IndentationError — перед @robust.safe є пробіли.

4 Повторити стрес-перевірку й прочитати журнал

cd ~/app
python3 stress.py
tail -n 8 ~/logs/app.log
python3 try_bot.py "/start"
tail -n 1 ~/logs/app.log

Тепер усі сімнадцять рядків мають бути добре, а наприкінці — Усе добре: бот пережив усі випадки. Тих, що падали, більше немає: довга відповідь обрізана до 4000 символів, помилка в «оновленні» зі словником-рядком перетворена на спокійну відповідь. У app.log з’явилися рядки на кшталт 2026-11-04 14:03:11 chat=999000111 текст ok 2ms len=1000 (довгий текст обрізано до 1000 символів ще до обробки), а останні — від try_bot.py: chat=1 /start ok 3ms len=6. Подивись уважно: тексту повідомлень у файлі немає, лише команда або слово «текст», довжина й номер чату.

Якщо stress.py ще падає, дивись останній рядок про «ПАДІННЯ». Для AttributeError чи KeyError усередині твого коду перевір, що @robust.safe стоїть безпосередньо над def handle_update, а не над іншою функцією. Перевір: grep -n -B1 "def handle_update" ~/app/bot.py має показати @robust.safe перед def.

5 Текст, схожий на SQL-команду

Це дослід із розділу 3. Зробимо його від імені адміна (твій номер з кроку 2 заняття 07, або 1, якщо Telegram немає й ти записав ADMIN_IDS=1):

cd ~/app
python3 try_bot.py --id 100200300 "/add Оля' OR '1'='1 | дослід"
sqlite3 ~/data/app.db "SELECT id, name FROM users ORDER BY id DESC LIMIT 1"

Бот відповідає Додано: 12. Оля' OR '1'='1. Усі контакти: /list, а база — 12|Оля' OR '1'='1. Ім’я записано дослівно, разом із лапками: дані лишилися даними. Якби запит склеювався з рядків, лапка з імені закрила б рядок у команді, OR '1'='1' став би частиною SQL, і в базі опинилося б не те, що ти надіслав, — або прийшла б помилка. Прибери за собою запис (номер — твій):

python3 try_bot.py --id 100200300 "/del 12"

Відповідь — Видалено: 12. …. Помилка Ця команда лише для власника бота. — ти пробуєш не зі своїм номером: підстав той, що в ADMIN_IDS.

6 Перезапустити службу й побачити журнал наживо

Службу треба перезапустити: вона читає код лише під час старту, і без цього живий бот працюватиме по-старому.

systemctl --user restart club-bot
sleep 3
systemctl --user is-active club-bot
tail -n 3 ~/logs/club-bot.log
tail -f ~/logs/app.log

Має бути active. Остання команда не повертає запрошення: вона показує нові рядки журналу, які щойно з’являються, — «стежить» за файлом. Напиши своєму боту в Telegram /start, потім /help, потім стікер: у вікні з’являться три рядки, у стікера — message без-тексту. Зупини стеження — Ctrl+C. Це перевіряє L08.2 (рядки журналу), а живу відповідь бота в Telegram бачить викладач. Без Telegram — покажи викладачеві вивід stress.py і tail -n 8 ~/logs/app.log.

7 Бій: Red Team проти Blue Team — за згодою

Пари призначає викладач. Одна людина в парі — Blue Team (власник бота, що обороняється), інша — Red Team (нападає). Через півгодини ролі міняються. Домовтеся вголос: «Можна я перевірю твого бота?» — «Так». Red Team отримує лише нік бота. Токен, паролі й логіни не називають нікому: напад іде ззовні, як від незнайомої людини.

Правила, які діють завжди. Слово «стоп» від Blue Team зупиняє бій одразу, без пояснень. Лише повідомлення боту в Telegram, жодного SSH і файлів. Повідомлення пишеш по одному й чекаєш відповіді, у циклі не надсилаєш. Паролів не підбираєш, у чужий акаунт не заходиш. Не більше одного /ask із питанням: ліміт ШІ спільний.

Підготовка Blue Team (3 хвилини). Заховай у своїй базі прапорець і зроби страхову копію:

cp /opt/club/kurs/08/arena.py ~/app/
cd ~/app
python3 arena.py start

Скрипт робить копію бази в ~/data/app.before-arena.db, ховає в контакті з іменем СЕКРЕТ рядок FLAG{…} і друкує відбиток прапорця: короткий код, за яким можна перевірити, чи прапорець справжній, не показуючи самого прапорця. Відбиток скажи Red Team вголос. Сам прапорець нікому не показуй. Тепер відкрий друге вікно термінала й тримай там tail -f ~/logs/app.log: у ньому видно кожне повідомлення нападника.

Раунд 1 (5 хвилин). Розвідка. Red Team пише боту /start і /help, читає список команд і записує в блокнот, які з них могли б показати чужі дані чи щось змінити. Blue Team дивиться в журнал і вголос називає кожен рядок нападника («бачу: чат 777, команда /help»). За кожен названий в межах хвилини рядок Blue Team отримує очко.

Раунд 2 (10 хвилин). Атака. Мета Red Team: прочитати прапорець через Telegram. Пробуйте по черзі повідомлення з таблиці й записуйте: що надіслали, що відповів бот.

Що надіслатиЩо має бутиЯкщо інакше — знахідка
/usersвідмова «лише для власника»показує імена людей — не закрито (заняття 07)
/del 999999відмова «лише для власника»«такого контакту немає» — замка на видаленні немає
/add ім’я (з одним словом)відмоваконтакт додано — замка немає (потім його треба видалити, скажи власникові)
стікербот мовчить або чемно відповідаєбот перестав відповідати на все
довге повідомлення (вставити великий текст)відповідь не довша за екранбот мовчить: відповідь задовга для Telegram
/ask без питанняпідказка «напиши питання»помилка чи мовчання
/qwerty (вигадана команда)«Не знаю такої команди»бот падає

Кожне повідомлення складено так, щоб нічого не зіпсувати, навіть якщо замка немає (хіба що /add: його одне слово додає один рядок, який після бою повернеться разом із відновленням бази). Придумай ще 2–3 свої нешкідливі «погані» повідомлення за зразком уроку. Прочитала рядок FLAG{…}? Перевір його на своєму акаунті:

cp /opt/club/kurs/08/arena.py ~/app/
python3 ~/app/arena.py verify ВІДБИТОК FLAG{...}

Побачила «✔ прапорець справжній: захоплено!» — скажи Blue Team особисто, яка саме команда його видала. Журнал у цей час показує Blue Team, як виглядає напад.

Раунд 3 (7 хвилин). Оборона. Blue Team вибирає знайдену дірку (або сама придумує, куди б нападник міг ще пройти), закриває її у своєму bot.py, перезапускає службу (systemctl --user restart club-bot, потім sleep 3) і просить Red Team повторити те саме повідомлення. Тепер має бути відмова або чемна відповідь. Не закрилося — шукайте разом, у чому справа.

Раунд 4 (3 хвилини). Відновлення. Після бою Blue Team прибирає прапорець і повертає базу, якою вона була до бою:

python3 arena.py end --restore
sleep 3
systemctl --user restart club-bot

Скрипт повертає базу зі страхової копії й прибирає прапорець. Це маленька репетиція відновлення з копії (кроки 8–10 того самого уроку). Перевір у Telegram, що бот відповідає на /start.

Ще картки для Red Team (бот). Беріть їх у раунді 2 за бажанням, кожна додає очки:

Що надіслатиЩо має бутиЯкщо інакше — знахідка
/add з іменем із 3000 літервідмова «ім’я задовге»запис додано: база роздувається, відповідь обрізається
/add з іменем із емодзі, невидимих пробілів і кількох переносів рядкачисте ім’я або відмовау списку «порожній» рядок або поламаний вивід
/add Тест'); -- (лише лапка й дві риски, нічого не видаляє)звичайний запис із цим текстомпомилка бази або дивна поведінка: код склеює запит рядком
/USERS, /users  із зайвим пробілом, /users@ІншийБотвідмова «лише для власника» або «не знаю такої команди»команда виконалась там, де не мала: розбір команди надто простий
змінити своє ім’я в Telegram на «Власник» і надіслати /usersвідмовабот пустив: він довіряє імені, а не номеру чату (ім’я змінює будь-хто за секунду)

Фронт сервера: перевірки Blue Team. У чужий акаунт не заходимо ні за яких умов, тому кожен перевіряє свою частину сервера, а Red Team дивиться на екран і називає, що вона б спробувала. Команди без прав адміністратора:

ДверіЩо виконати Blue TeamЯк виглядає добреЯк закрити
Файли з секретамиls -l ~/app/.env ~/data/app.db-rw-------: читає лише власникchmod 600 ~/app/.env ~/data/app.db
Токен у Gitcd ~/app && git log -p | grep -c "BOT_TOKEN="0, жодного рядка зі справжнім токеномперевипустити токен у BotFather, а потім уже чистити історію (стара версія лишається в Git)
Автозапускsystemctl --user show club-bot -p RestartRestart=alwaysдодати Restart=always у файл служби, systemctl --user daemon-reload
Місце на дискуdf -h ~ і du -sh ~/* | sort -h | tailвільно більше 10%, немає величезних файлівприбрати зайве, обмежити журнал
Журналtail -n 20 ~/logs/app.logрядок на кожне повідомлення нападника, без текстуякщо рядків бракує, підключи щит robust.py (крок 3)

Правило Blue Team для кожної дірки: змінив → застосував → перевірив. Змінив файл, але не перезапустив службу — це ще не закрито. «Закрив» без команди й виводу, які це доводять, — просто припущення. І ще одне: спершу збережи докази, потім ремонтуй (cp ~/logs/app.log ~/evidence.log).

Раунд-сюрприз від викладача (5 хвилин). Викладач каже «Інцидент!» й оголошує одне з трьох: «нічний гість» (в журналі багато рядків від одного нападника: порахуйте й назвіть); «токен у старому коміті» (знайдіть у власній історії Git рядок BOT_TOKEN= командою з таблиці й поясніть, чому недостатньо видалити файл: що робити першим кроком); «служба впала» (systemctl --user kill -s SIGKILL club-bot: побачте це в журналі й доведіть, що служба підвелась сама). Інцидент не покарання, а перевірка, чи працює формула «помітити → закрити → перевірити» під тиском.

Звіт про інцидент (п’ять рядків, пише Blue Team у BUGS.md). Що сталося (один факт). Коли й де помітили (час і рядок журналу). Що зробили (кроки по порядку). Що закрили і як перевірили (команда й вивід). Що змінимо, щоб не повторилося (одна дія). Чого не знаєш, пишеш «невідомо», без здогадок.

Рахунок бою (веде викладач)Red TeamBlue Team
Захоплений прапорець+5—
Окрема слабка точка з таблиці чи своя (кожна)+1 (до 7)—
Бот замовк після атаки+2—
Напад помічено й названо вголос за хвилину—+1 (до 5)
Дірку закрито, і повтор не пройшов—+2 за кожну
Бот відповідає на /start після всіх раундів—+2
Відновлення пройшло, база на місці—+1

Розбір після бою (Purple Team, 5 хвилин). Сядьте разом і відповідайте на три питання: що спрацювало, що проґавили, що змінимо. Знахідки Red Team каже Blue Team особисто (не в загальний чат, поки дірку не закрито); Blue Team записує їх у BUGS.md (крок 14) одним рядком із позначкою, хто знайшов, а Red Team перечитує запис і підтверджує. Не знайшли нічого — скажіть викладачеві: «перевірили, слабких місць не знайшли». Це теж результат. Позначку L08.7 ставить викладач.

8 Зробити першу копію бази

Повернись до свого терміналу. Візьми готові скрипти уроку й подивись, як влаштовано бекап:

cp /opt/club/kurs/08/backup.py /opt/club/kurs/08/restore.py ~/app/
cd ~/app
less backup.py

Головне в ньому: from db import DB_PATH — шлях до бази з того самого db.py, що й у служби, тож копіюється саме база бота; target = BACKUPS / f"app-{datetime.now():%Y%m%d-%H%M}.db" — ім’я з датою й часом до хвилини; src.backup(dst) — цілісна копія засобами SQLite (теорія, розділ 5); sorted(BACKUPS.glob("app-*.db"))[:-KEEP] — усі копії за алфавітом, тобто за часом, крім останніх KEEP = 14, і їх old.unlink() видаляє; наостанок print(target) друкує, куди покладено копію. Вийти з less — q. Тепер зроби копію й переконайся, що в ній те саме, що й у базі:

COPY=$(python3 backup.py)
echo $COPY
ls -l ~/backups
sqlite3 "$COPY" "SELECT COUNT(*) FROM users"
sqlite3 ~/data/app.db "SELECT COUNT(*) FROM users"

COPY=$(…) зберігає надрукований скриптом шлях у змінну, і echo покаже його: /home/bohdan.g9/backups/app-20261104-1440.db. У ~/backups лежить цей файл розміром приблизно як ~/data/app.db. Два останні числа мають збігтися: скільки людей у копії, стільки й у базі. Порожній рядок після echo — скрипт упав: запусти python3 backup.py без COPY= і прочитай помилку. No module named 'yaml' — запускай /opt/club/venv/bin/python3 backup.py; No module named 'db' — ти не в ~/app.

9 Увімкнути щоденний таймер

cp /opt/club/kurs/08/club-backup.service /opt/club/kurs/08/club-backup.timer ~/.config/systemd/user/
cat ~/.config/systemd/user/club-backup.service ~/.config/systemd/user/club-backup.timer

У club-backup.service головне — Type=oneshot («запустити, дочекатися кінця й нічого не тримати», на відміну від club-bot, яка працює постійно) і ExecStart=/opt/club/venv/bin/python3 %h/app/backup.py, де %h systemd замінює на твою домашню теку. У club-backup.timer — OnCalendar=*-*-* 03:30 і Persistent=true (теорія, розділ 5).

systemctl --user daemon-reload
systemctl --user enable --now club-backup.timer
systemctl --user list-timers club-backup.timer

daemon-reload просить systemd перечитати файли — нових він сам не помічає. enable --now вмикає таймер назавжди й одразу; у відповідь — Created symlink …/club-backup.timer. list-timers покаже один рядок: NEXT — найближчі 03:30, LEFT — скільки до них лишилося, LAST — порожньо або - (таймер ще не спрацьовував), ACTIVATES — club-backup.service. Чекати ночі не треба — запусти службу бекапу руками:

systemctl --user start club-backup.service
tail -n 5 ~/logs/club-backup.log
ls -l ~/backups

Для служби oneshot команда start чекає, поки копія буде готова, — секунду-дві. У файлі журналу ~/logs/club-backup.log лише шлях до нової копії. Що служба відпрацювала успішно, показує systemctl --user status club-backup.service --no-pager: у рядку Active має стояти inactive (dead), а в Process — status=0/SUCCESS. У ~/backups тепер дві копії (або одна, якщо ти встиг у ту саму хвилину, що й у кроці 10: файл із тим самим ім’ям просто оновився). Unit … not found чи does not exist — файли не скопійовано або забуто daemon-reload.

10 Відновитися з копії на пробу

python3 restore.py

Скрипт бере найсвіжішу копію з ~/backups, розгортає її в ~/restore-test/app.db і друкує, що там:

users: записів — 9
відновлено з app-20261104-1442.db у /home/bohdan.g9/restore-test/app.db

Числа — твої: скільки людей у таблиці users (заняття 04). Що в restore.py (подивись less restore.py): без аргументу береться copies[-1] — найсвіжіша копія, а можна вказати конкретну: python3 restore.py ~/backups/app-20261104-1440.db. Копію відкрито лише для читання (mode=ro, від англійського read only), тож відновлення не зіпсує сам бекап; далі той самий src.backup(dst), тільки в протилежний бік. Назву таблиці в SELECT COUNT(*) вставлено f-рядком, і це безпечно: вона прийшла із самої бази (sqlite_master), а не від людини. Переконайся, що копія справжня:

sqlite3 ~/restore-test/app.db "SELECT name, messages FROM users ORDER BY messages DESC LIMIT 3"

Троє людей, що писали боту найбільше, — ті самі, що й у /list у боті. Справжньої бази ~/data/app.db ніхто не чіпав.

Так виглядає справжнє відновлення — сьогодні його не виконуйЯкщо колись ~/data/app.db зіпсується: systemctl --user stop club-bot (щоб бот не писав у базу під час заміни), python3 restore.py і уважно прочитати вивід, cp ~/restore-test/app.db ~/data/app.db, chmod 600 ~/data/app.db, systemctl --user start club-bot. Усе, що змінилося після останньої копії, буде втрачено, — тому копії й роблять щодня.

11 Перевірити, хто може прочитати копії

На занятті 07 ти закрив ~/data/app.db від сусідів. А копії?

ls -ld ~ ~/backups ~/data ~/restore-test
ls -l ~/data/app.db ~/restore-test/app.db

-d показує права самої теки, а не її вмісту. Найімовірніше, побачиш таке (крапка після прав — позначка системи захисту SELinux, на неї не зважай):

drwx--x--x. 14 bohdan.g9 bohdan.g9  4096 Nov  4 14:42 /home/bohdan.g9
drwxr-x---.  2 bohdan.g9 bohdan.g9  4096 Nov  4 14:41 /home/bohdan.g9/backups
drwxr-x---.  2 bohdan.g9 bohdan.g9  4096 Nov  4 14:30 /home/bohdan.g9/data
drwxr-xr-x.  2 bohdan.g9 bohdan.g9  4096 Nov  4 14:42 /home/bohdan.g9/restore-test
-rw-------.  1 bohdan.g9 bohdan.g9 24576 Nov  4 14:40 /home/bohdan.g9/data/app.db
-rw-r--r--.  1 bohdan.g9 bohdan.g9 24576 Nov  4 14:42 /home/bohdan.g9/restore-test/app.db

Як на занятті 07, дивись на останні три літери прав — «інші», тобто будь-який користувач сервера. Домашня тека — --x: пройти крізь неї можна, а подивитися, що в ній, — ні. backups і data — ---: сусід туди навіть не зайде, тож копії в ~/backups захищені, хоч самі файли там -rw-r--r--. А restore-test — r-x (може бути й drwxrwxr-x, «інші» там однакові), і app.db у ній — r--: будь-хто на сервері, хто знає шлях, прочитає повну копію твоєї бази — імена однокласників і їхні ніки.

Це знахідка, і саме такі трапляються найчастіше: нова тека й новий файл отримали права «за замовчуванням», а chmod 600 ти робив лише для справжньої бази. Закрий:

chmod 700 ~/restore-test
chmod 600 ~/restore-test/app.db
ls -ld ~/restore-test
ls -l ~/restore-test/app.db

Тепер drwx------ і -rw-------. Наступні запуски restore.py пишуть у той самий файл, тож права збережуться. Запиши знахідку в BUGS.md у кроці 14. Якщо в тебе одразу були drwx------ і -rw-------, знахідки немає, і це теж результат.

12 Подивитися, які двері сервера відчинені, і скільки їсть пам’яті бот

ss -ltn
systemctl --user show club-bot -p MemoryMax -p MemoryCurrent -p Restart -p NRestarts
free -m

ss -ltn друкує таблицю портів, що слухають (теорія, розділ 8). Найважливіший стовпець — Local Address:Port:

State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN 0      128          0.0.0.0:22         0.0.0.0:*
LISTEN 0      511          0.0.0.0:80         0.0.0.0:*
…

0.0.0.0:22 і 0.0.0.0:80 — SSH і nginx, вони чекають з’єднань звідусіль. Рядка з твоїм ботом тут немає — і не має бути: бот нічого не слухає. Друга команда показує межі й стан твоєї служби: MemoryMax=104857600 (це 100 МБ у байтах), у MemoryCurrent — скільки бот займає зараз (десятки мегабайтів), Restart=always і NRestarts=0 — служба ще не перезапускалася сама. free -m — вільна пам’ять усього сервера: стовпець available маленький, бо на сервері багато службових програм; ось чому кожна зайва служба, що тримає порт і чекає запитів, на ньому дорога.

Подивись, що бот зараз робить із мережею: ss -tn | grep ":443". Один-два рядки зі станом ESTAB — це з’єднання твого бота (та інших служб) з Telegram: бот сам пішов до нього. Якщо рядків немає в цю мить, повтори команду через секунду: бот щойно відповів Telegram і відкриває нове з’єднання. Чужих процесів, служб і портів не чіпай.

13 Влаштувати аварію: kill -9

Остання перевірка на міцність — найгрубіша зупинка, як у L08.6. Сигнал SIGKILL (його номер — 9, звідси kill -9) програма навіть не отримує: система просто прибирає її з пам’яті посеред роботи. Команда нижче надсилає саме його процесу твоєї служби.

systemctl --user kill -s SIGKILL club-bot
systemctl --user is-active club-bot
sleep 8
systemctl --user is-active club-bot
python3 -c "import json,os; print(json.load(open(os.path.expanduser('~/data/bot_state.json')))['last_poll'])"
date +%Y-%m-%dT%H:%M:%S
tail -n 5 ~/logs/club-bot.log

Перше is-active, найімовірніше, скаже activating: systemd відраховує свої три секунди (RestartSec=3). Після sleep 8 — active, час у last_poll — на секунди-десятки секунд старший за поточний (date): бот знову опитує Telegram. У club-bot.log — рядок про старт нової служби. Напиши боту в Telegram — він відповідає: Restart=always підняв службу, а з нею й бота. Offset (що вже прочитано з Telegram) бот зберігає у файлі, тому після аварії не відповість двічі на ті самі повідомлення. failed замість active — дивись «Якщо щось не працює».

14 Записати знахідки в BUGS.md

Знахідка — слабке місце, яке ти справді перевірив: що зробив, що сталося, чому так і як закрив. Підходять знахідки із сьогоднішніх кроків, від однокласника й ті, що ти закрив раніше на курсі, — але лише справжні: якщо бот на 5000 літер одразу відповів нормально, писати «раніше падав» не можна. Кожна знахідка — один рядок, що починається з дефіса й пробілу (- ): так їх рахує L08.5. Ще одне джерело — питання «що було б, якби…»: якби база зіпсувалася, якби хтось спитав, хто й коли звертався до API.

nano ~/app/BUGS.md

Ось як це могло вийти в Богдана (у тебе — свої знахідки й свої слова):

# Що знайшли під час перевірки на міцність

- Що зробив: відновив бекап у ~/restore-test. Що сталося: копія бази мала права -rw-r--r--, її з іменами людей і хешем пароля міг прочитати будь-хто на сервері. Чому: нові файли отримують права за замовчуванням, а chmod 600 я робив лише для ~/data/app.db. Як закрив: chmod 700 ~/restore-test і chmod 600 ~/restore-test/app.db.
- Що зробив: спитав себе, що буде, якщо зіпсується ~/data/app.db. Що сталося: не було жодної копії, усі контакти пропали б назавжди. Чому: бекап ніхто не налаштовував. Як закрив: backup.py і таймер club-backup.timer щодня о 03:30, відновлення перевірив через restore.py.
- Що зробив: надіслав боту стікер і 5000 літер (stress.py). Що сталося: відповідь «Ти написав: …» вийшла довша за 4096 символів, Telegram її не прийняв, людина лишилась без відповіді. Чому: бот повторював весь текст без обмеження. Як закрив: @robust.safe обрізає відповідь до 4000 символів; stress.py тепер пише «добре».

Знахідку від однокласника позначай його ім’ям: «Марта знайшла…». Прізвищ і логінів не пиши: файл піде на GitHub. Порахуй, скільки вийшло:

grep -c "^- " ~/app/BUGS.md

Має бути 3 або більше. Менше — перевір, чи кожна знахідка в одному рядку й чи починається саме з - , а не з * чи — (довгий рядок nano показує в кілька екранних рядків, але у файлі він один). Покажи файл однокласникові, з яким ви обмінювалися перевірками: його знахідки мають бути записані так, як він їх знайшов.

15 Створити SSH-ключ і зайти на сервер без пароля

Цей крок робиш на своєму комп’ютері (не на сервері). У Windows відкрий програму PowerShell (клавіша Windows, набери PowerShell, Enter). Створи пару ключів:

ssh-keygen -t ed25519 -C "shkola-key"

На питання про місце збереження просто натисни Enter. Якщо програма питає «Overwrite (y/n)?», тобто такий ключ у тебе вже є, натисни n: не перезаписуй його, а візьми готовий публічний ключ командою нижче. Далі програма попросить парольну фразу — це додатковий пароль, який захищає ключ, якщо його вкрадуть; придумай і запиши її (або натисни Enter, щоб без неї, але з нею надійніше). Після цього з’являться два файли у теці .ssh твого профілю. Покажи публічний ключ — це один довгий рядок, який починається з ssh-ed25519:

type $env:USERPROFILE\.ssh\id_ed25519.pub

Скопіюй увесь рядок (можна одразу в буфер: type $env:USERPROFILE\.ssh\id_ed25519.pub | clip). Для Linux чи macOS: cat ~/.ssh/id_ed25519.pub. У всіх командах заміни <логін> разом із кутовими дужками на свій. Тепер зайди на сервер (поки ще за паролем) і поклади ключ у спеціальний файл, який сервер читає при вході:

ssh <логін>@91.219.61.4
mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys

У редакторі nano вставити скопійований рядок (правою кнопкою миші). Якщо у файлі вже є рядки — не стирай їх, вставляй свій у новий рядок унизу. Потім зберегти — Ctrl+O, Enter, вийти — Ctrl+X. Потім закрий файл для сторонніх (сервер відмовиться від ключа, якщо файл відкритий усім) і вийди:

chmod 600 ~/.ssh/authorized_keys
exit

Тепер перевір: знову на своєму комп’ютері:

ssh -i $env:USERPROFILE\.ssh\id_ed25519 <логін>@91.219.61.4

Якщо ти поставив парольну фразу — сервер тебе не питає пароль від акаунта, а лише фразу ключа; без фрази — зайде одразу. Побачив привітання сервера — готово: ти зайшов ключем, а не паролем. Якщо питає пароль від акаунта, ключ не спрацював: див. «Якщо щось не працює» нижче.

Приватний файл id_ed25519 (без .pub) нікому не пересилай і не вставляй у ШІ чи на GitHub. Це крок для знайомства: пароль на нашому сервері лишається, а ключем ти вчишся входити так, як це роблять на справжніх серверах.

Зберегти роботу на GitHub

У Git ідуть код і BUGS.md. Журнал, копії бази й ~/restore-test — ні: це дані з іменами людей, і лежать вони поза ~/app, тож Git їх навіть не бачить. Файли таймера живуть у ~/.config/systemd/user/, теж поза репозиторієм, а їхні оригінали є в /opt/club/kurs/08/.

16 Зробити коміт і відправити роботу

cd ~/app
git status --short
git add -A
git commit -m "Заняття 08: щит robust.py, журнал оновлень, бекап і відновлення, BUGS.md"
git push origin main
git push github main

git status --short покаже M bot.py (змінено) і ?? BUGS.md, ?? backup.py, ?? restore.py, ?? robust.py, ?? stress.py (нові); якщо ти виправляв ще щось — і ці файли. Бачиш будь-який .db чи app.log — ти створив їх у ~/app замість домашньої теки: видали (rm) і повтори git status --short. git commit відповість [main …] Заняття 08: … і 6 files changed; кожен git push закінчиться рядком main -> main — перший оновлює ~/repo.git, другий — GitHub. На GitHub з’явилися BUGS.md, backup.py, restore.py, robust.py і stress.py, а журналу й копій бази немає — і не має бути.

Що перевіряє автоматика

Сьогодні автоперевірка заходить на сервер від твого імені й дивиться вісім пунктів. L08.1 бере твій bot.py в ізольованому оточенні (власна домівка, копія бази, підставна «ШІ») і надсилає боту сімнадцять «поганих» оновлень — ті самі, що в stress.py: 5000 літер, емодзі, стікер, порожнє оновлення, відредаговане повідомлення, кнопка без повідомлення та інші. Пункт зараховано, якщо жодне не валить handle_update, відповідь завжди текст (або мовчання) і не довша за 4096 символів. Якщо ні — в повідомленні видно, яке саме оновлення підвело. L08.2 надсилає шість оновлень і дивиться в тимчасовий ~/logs/app.log: у ньому має бути не менше шести рядків із датою й часом, номер чату й команда /help, але не текст повідомлення. Справжній твій журнал перевірка не чіпає.

L08.3 бере найсвіжіший файл у ~/backups: йому має бути не більше двох діб, і він має відкриватися як база; крім того, таймер club-backup.timer має бути ввімкнено. L08.4 відкриває ~/restore-test/app.db, звіряє список таблиць зі справжньою базою й перевіряє, що головна таблиця в копії не порожня. L08.5 рахує в ~/app/BUGS.md рядки, що починаються з «- » і довші за дванадцять символів, — потрібно щонайменше три; що в них написано, читає викладач. L08.6 вбиває процес бота, як ти в кроці 13, і чекає кілька секунд, поки служба club-bot підніметься: бот на ці секунди замовкне. L08.7 ставить викладач: що чужого бота ти перевіряв лише з дозволу власника. L08.8 дивиться на ~/.ssh і authorized_keys: права мають бути закриті, а у файлі — публічний ключ ssh-ed25519. Натисни «Перевірити» в розділі «Перевірка виконаних завдань» нижче на сторінці. Служба має працювати саме в цю мить, тож перевіряй під час заняття або ввечері, коли служби групи ввімкнено.

КодЩо перевіряєтьсяЯкщо не зараховано
L08.1«погані» повідомлення не валять бота, відповідь не довша за 4096
автоперевірка
Один зі спеціально зіпсованих апдейтів (5000 літер, стікер, порожнє оновлення…) поклав бота або дав задовгу відповідь — у повідомленні видно, який саме. Підключи щит: import robust і @robust.safe над def handle_update; спершу python3 stress.py покаже те саме.
L08.2кожне оновлення записано в ~/logs/app.log із датою й часом, без тексту
автоперевірка
Після кількох повідомлень у ~/logs/app.log немає рядка про кожне з них (з датою, часом, номером чату й командою), або в журнал потрапив текст повідомлення. Готовий robust.py робить усе це сам.
L08.3свіжий бекап, який відкривається
автоперевірка
У ~/backups немає свіжої копії app-….db з даними або не ввімкнено таймер. Увімкни щоденний таймер: systemctl --user enable --now club-backup.timer
L08.4відновлення з бекапу перевірено
автоперевірка
Немає ~/restore-test/app.db або в ньому бракує таблиць. Запусти python3 restore.py і подивись вивід.
L08.5BUGS.md: щонайменше 3 знахідки з причиною
автоперевірка
У ~/app/BUGS.md менше трьох пунктів. Кожен — окремим рядком «- …»: що зробили, що сталося, чому, як закрили.
L08.6після kill -9 служба club-bot піднімається сама
справність сервісу, без балів
Служба не піднялась після kill -9. Потрібен Restart=always у ~/.config/systemd/user/club-bot.service; причину дивись у tail -n 30 ~/logs/club-bot.log
L08.7знахідки в боті однокласника — лише з його дозволу, і він їх підтвердив
викладач
Позначку ставить викладач на занятті або після нього.
L08.8вхід за SSH-ключем налаштовано: ключ лежить на сервері з правильними правами
автоперевірка
chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys; у файлі має бути один рядок, що починається з ssh-ed25519 (публічний ключ, файл id_ed25519.pub, не приватний).

Твої відповіді на питання уроку

Кожне питання оцінюється відразу в браузері — це лишається так само. Ця кнопка лише надсилає підсумок учителю, щоб відповіді не зникли разом із закритою вкладкою.

Перевірка виконаних завдань

Список критеріїв і що саме вони означають — у таблиці вище, «Що перевіряє автоматика». Тут — та сама перевірка, але прямо зараз, без очікування на найближчий прохід за розкладом.

Домашнє завдання

  1. Закрий усе, що знайшов у тебе однокласник. На кожну знахідку порядок однаковий: повтори його повідомлення й переконайся, що проблема відтворюється; знайди причину в журналі (tail -n 30 ~/logs/club-bot.log і tail -n 30 ~/logs/app.log) або в правах (ls -l, як у кроці 11); виправ саму причину, а не наслідок; перезапусти службу й повтори — тепер відповідь спокійна. Допиши в рядок у BUGS.md, як закрив, і попроси однокласника повторити свою перевірку.
  2. Переконайся, що таймер працює без тебе. Завтра, після 03:30:
    systemctl --user list-timers club-backup.timer
    ls -l ~/backups
    cd ~/app && python3 restore.py
    У стовпці LAST має стояти сьогоднішня ніч, у ~/backups — копія з часом 0330 в імені, а restore.py має відновити саме її. Якщо нової копії немає, подивись tail -n 20 ~/logs/club-backup.log.
  3. Прочитай свій журнал, як детектив.
    awk '{print $4}' ~/logs/app.log | sort | uniq -c | sort -rn | head
    grep -c "ПОМИЛКА" ~/logs/app.log
    awk '{print $3}' ~/logs/app.log | sort | uniq -c | sort -rn | head -n 3
    awk '{print $4}' друкує четверте поле кожного рядка — команду, а sort | uniq -c | sort -rn рахує, скільки разів трапилась кожна, найчастіші першими. grep -c "ПОМИЛКА" — скільки разів щит упіймав помилку. Третя команда друкує номери чатів (третє поле), з яких писали найчастіше. Запиши в чернетку: які команди найпопулярніші, чи були помилки і від кого найбільше повідомлень.
  4. Підготуйся пояснити. Чому бекап роблять через src.backup(), а не cp; чому відновлюють спершу в ~/restore-test; чому в app.log немає тексту повідомлень; чому в бота немає порту; як перевірити замок у чужого бота, нічого не зіпсувавши. На початку заняття 09 викладач спитає щось одне з цього.
  5. Збережи. git add -A, git commit -m "Заняття 08: закрито знахідки однокласника", git push origin main і git push github main.

Якщо щось не працює

Вхід за ключем не працює: сервер знову питає пароль

Найчастіші причини, по черзі:

Після правки bot.py служба не стартує або бот мовчить

systemctl --user status club-bot --no-pager
tail -n 30 ~/logs/club-bot.log
cd ~/app && /opt/club/venv/bin/python3 -c "import bot" && echo готово

Остання команда показує помилку, не чекаючи служби. Що означає останній рядок:

Погані повідомлення ще валять бота (L08.1)

Повідомлення називає випадок, наприклад BAD: «дуже довге повідомлення»: відповідь 5012 символів… чи «message — не словник» валить бота: AttributeError.

Перед повторною перевіркою запусти python3 stress.py: це ті самі випадки, і видно, який саме падає.

У ~/logs/app.log порожньо або перевірка L08.2 не зараховує

python3 try_bot.py "/start"
tail -n 3 ~/logs/app.log
ls -ld ~/logs
grep -n "robust" ~/app/bot.py

backup.py або таймер не працюють (L08.3)

cd ~/app && python3 backup.py
ls ~/.config/systemd/user/
systemctl --user is-enabled club-backup.timer
systemctl --user list-timers club-backup.timer
tail -n 20 ~/logs/club-backup.log

restore.py: немає копії, файл не відкривається або бракує таблиць (L08.4)

Після kill -9 служба не піднялася (L08.6)

systemctl --user status club-bot --no-pager
grep -E "^Restart" ~/.config/systemd/user/club-bot.service

BUGS.md не зараховано (L08.5)

ls -l ~/app/BUGS.md
grep -c "^- " ~/app/BUGS.md
grep -n "^- " ~/app/BUGS.md | cut -c1-60