Перед уроком
Обери свою групу й себе зі списку. Тоді відповіді й перевірки завдань запишуться саме на тебе.
Знайти слабкі місця свого бота й закрити їх: погані повідомлення його не валять, кожне оновлення лишає рядок у журналі, база копіюється щодня і з копії вже відновлювались, а служба піднімається після аварії. Чужого бота перевіряти лише з дозволу власника.
Замість <логін> підставляєш свій
логін виду bohdan.g9: ім’я, крапка, номер групи. Кутові дужки не пишуться.
Кабінет — сторінка, де проти кожного критерію стоїть відмітка, зарахований він чи ні.
У прикладах на цій сторінці твій логін — bohdan.g9, а логін однокласника,
сервіс якого ти перевіряєш, — marta.g9. Обидва читай як справжні логіни й
виправляй у кожній команді, яку копіюєш.
Досі ти перевіряв, що бот працює: відповідає на /start, записує людей у базу, пускає до списку лише тебе. Сьогодні перевіриш, що він не ламається, коли все йде не за планом: хтось надсилає повідомлення на п’ять тисяч літер, одні емодзі, стікер замість тексту чи порожнє оновлення, процес служби раптом убивають, а файл бази псується. Так роблять професійні розробники (ті, хто пише програми за гроші) перед тим, як показати програму людям, і називають це перевіркою на міцність.
До кінця заняття в бота з’явиться:
robust.py над handle_update(): погане повідомлення не валить бота, а відповідь людині ніколи не довша за межу Telegram (4096 символів);~/logs/app.log — рядок із датою й часом на кожне повідомлення, яке отримав бот (без самого тексту);~/backups зберігаються 14 останніх;~/restore-test, і видно, що таблиці на місці;BUGS.md — щонайменше три знахідки: що ти зробив, що сталося, чому й як ти це закрив.Показати можна буде: вивід stress.py — «добре» на всі погані повідомлення; рядки app.log, які з’являються наживо; копії в ~/backups і вивід restore.py; бота, що сам піднявся після kill -9; BUGS.md на GitHub.
Сьогодні ми захищаємося, а не нападаємоПеревіряєш лише свого бота. Бота однокласника — тільки з його дозволу, тільки повідомленнями в Telegram — і так, щоб нічого не зіпсувати. Заходити в чужий акаунт, відкривати чужі файли, підбирати паролі, засипати бота повідомленнями не можна навіть «для перевірки». Знайдене кажеш власникові, а не використовуєш. Чому так — у першому розділі теорії.
Надійний бот не «втрачає голову», коли все пішло не так: на погані дані чемно відмовляє, після аварії сам піднімається, а втрачену базу можна повернути з копії. Перевірити це можна одним способом — навмисно створити погану ситуацію й подивитися, що вийде. Вміння ламати своє — частина професії розробника.
Ті самі дії з чужим сервісом без дозволу — уже не перевірка, а втручання: межу визначає не те, що ти робиш, а те, чи дозволив власник. Фахівців, які шукають слабкі місця за згодою власника, називають «білими хакерами», і компанії навіть платять їм за знахідки. Без згоди те саме — порушення. У законі це звучить як «несанкціоноване втручання в роботу комп’ютерних систем» (стаття 361 Кримінального кодексу України) і вважається злочином. Простими словами: заходити туди, куди тебе не пускали, не можна. А на нашому сервері діє ще й правило курсу: у чужий акаунт не заходять, чужих файлів не відкривають, паролів не підбирають.
Тому сьогоднішня перевірка однокласника має чотири умови:
/del 999999 до запису, якого не існує: працює замок — прийде відмова «лише для власника», не працює — «такого контакту немає», і все одно нічого не видалено. Це як смикнути ручку дверей, а не виламувати їх.Як користуватися ШІ-помічникомМожна попросити ШІ пояснити незнайомий рядок помилки або підказати, які ще погані дані спробувати на своєму сервісі. Не вставляй у чат .env, BOT_TOKEN і рядки з бази — там секрети й імена людей. Не запускай на спільному сервері команд, яких не розумієш. Виправлення пиши сам, а в BUGS.md пояснюй своїми словами: на занятті тебе попросять розповісти, що саме було не так.
Марта перевіряла бота Петра за його згодою й помітила: команда /users показує імена всіх, хто писав боту, кожному, хто її надішле, — Петро не доробив заняття 07. Що їй робити?
Великі компанії, банки й армії перевіряють свою безпеку не лише списками правил, а навчальними боями. Беруть двох «суперників» усередині одного колективу:
| Команда | Хто це | Що робить | Чим виграє |
|---|---|---|---|
| Red Team («червоні») | нападники | шукають слабкі місця так, як це зробив би справжній зловмисник, але з дозволу й за правилами | знаходить дірку й показує, що вона справжня |
| Blue Team («сині») | захисники | стежать за журналами, помічають напад, закривають дірки й відновлюють роботу | вчасно бачить напад і не дає нічого вкрасти чи зламати |
Назви прийшли з військових навчань: «червоні» грають за супротивника, «сині» — за своїх. Коли бій скінчився, обидві команди сідають разом і розбирають, що сталося: де нападники пройшли, де захисники помітили, а де проґавили. Таке спільне розбирання інколи називають Purple Team («фіолетові» — суміш червоного й синього). Саме тут народжується користь: гра не заради перемоги, а заради того, щоб після неї захист був міцнішим.
Чому гра, а не справжній нападЗахист, який жодного разу не перевіряли нападом, — це лише надія. Справжній злочинець не скаже, що пройшов. А товариш по команді скаже й допоможе закрити дірку. Тому нападати дозволено лише в грі, за згодою господаря, з правилами й словом «стоп». Без цього те саме було б правопорушенням.
FLAG{…} (допоможе скрипт arena.py). Red Team має прочитати його через Telegram. Хто прочитав, той «захопив прапорець».Червона команда першою знайшла, що в боті немає замка на команді /users. Що найкраще для бою?
Хто виграє від навчального бою Red Team проти Blue Team?
Бот пускає до /users тих, чиє ім’я в Telegram — «Власник». Що не так?
Red Team знайшла у старому коміті справжній токен бота, а Blue Team видалила файл новим комітом. Витік закрито?
Blue Team змінила службу бота, але не виконала daemon-reload і не перезапустила її. Вона каже: «Закрито». Що відповіси?
Бот — це програма, яка приймає дані від незнайомих людей. А люди (і програми, які за них пишуть) надсилають усяке: порожнє повідомлення, стікер замість тексту, /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"]. Що станеться без щита?
На занятті 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 — бот перевіряє це за списком раніше, — а самі значення й тут ідуть через ?. Правило: у текст запиту можна вставляти лише те, що написав автор коду, а все, що прийшло ззовні, — тільки через ?.
Журнал (його ще називають логом) — рядки, які програма пише про свою роботу: що сталося й коли. Сьогодні твій бот почне записувати кожне оновлення у файл ~/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 там нічого. Отже, вона вигадує». Що відповісти Данилові?
Уся пам’ять бота — один файл ~/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 вони врятують, а від загибелі сервера — ні. Тому професіонали тримають ще одну копію деінде. Викладач покаже це на своєму екрані: перенесе бекап на інший сервер і відкриє його там.
Ці історії вигадані, але такі випадки справді трапляються — і в школярів, і в дорослих.
DELETE FROM users і випадково натиснув Enter, не написавши умову WHERE. З таблиці зникли всі люди, що писали боту. Копія, зроблена вночі, — і через п’ять хвилин усе повернулося, втрачено лише записи за кілька годин. Без копії довелося б починати з нуля.Висновок простий: копія корисна, коли вона регулярна (щодня, а не «коли згадаю»), кількох поколінь (не лише остання) і перевірена (ти вже хоч раз відновився з неї).
Марко два місяці щоночі робив бекап власною командою. Коли база зламалася, він відкрив копії — усі порожні: команда копіювала не той файл. Що допомогло б помітити це на два місяці раніше?
Щойно в інтернеті з’являється новий сервер, до нього починають стукати автоматичні програми — не твій 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.
fail2ban блокує адресу, яка забагато разів помилилась. (На нашому навчальному сервері вона зараз вимкнена, бо 30 учнів з однієї школи часто помиляються в паролі й самі себе заблокували б.)На сервері за тиждень 81 тисяча невдалих спроб входу. Що це означає?
SSH-ключ — це пара файлів, які створюються разом. Приватний ключ лишається в тебе на комп’ютері й нікому не показується. Публічний ключ можна давати будь-кому: ти кладеш його на сервер. Коли заходиш, сервер каже «доведи, що в тебе є пара до цього публічного ключа», і комп’ютер це доводить — а сам приватний ключ мережею не передається.
Порівняння: вхід за SSH — це пара «замок і ключ від нього». Замок (його файл називають публічним ключем) ти вішаєш на двері сервера, а ключ від замка (приватний ключ) лишається лише в тебе. Замків можна скільки завгодно повісити, а ключ від них — один, і він у тебе. Підібрати його роботу неможливо: приватний ключ — це число, у якому сотні знаків, а не слово, яке можна вгадати.
Вигоди ключа порівняно з паролем:
Важливо: приватний ключ (id_ed25519, без .pub) — як пароль: нікому не надсилай, не клади на GitHub і не вставляй у ШІ. Публічний (id_ed25519.pub) можна показувати. На справжніх серверах пароль взагалі вимикають і лишають тільки ключі. На нашому навчальному сервері пароль залишено, бо ви всі заходите вперше й помилки в налаштуванні ключа не мають закрити вам доступ.
Який із двох файлів ключа можна покласти на сервер, а який ніколи нікому не показують?
Сервер схожий на будинок із багатьма дверима. Кожні двері — це порт, номер від 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 ГБ на всіх):
club-bot має межі: не більше 100 МБ пам’яті й чверть одного ядра. Навіть якщо твій код зациклиться чи роздме пам’ять, сервер працюватиме для інших, а systemd просто перезапустить службу;/ask «для перевірки» з’їдять ліміт усього класу;~/www з однієї адреси.Богдан запускає 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.
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/git status --short нічого не друкує — усе із заняття 07 закомічено. Видно файли — спершу git add -A і git commit -m "Заняття 07: доробки";main (перейменовано на занятті 05). Бачиш master — у кроці 16 пиши master замість main;active — служба працює; inactive — systemctl --user start club-bot (поза розкладом групи служби вимкнено);grep -c рахує рядки з access.check: має бути 1. 0 — заняття 07 не доведено до кінця, і наступні кроки цього не виправлять;backup.py club-backup.service club-backup.timer restore.py robust.py stress.py. No such file or directory — скажи викладачеві.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.
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 є пробіли.
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.
Це дослід із розділу 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.
Службу треба перезапустити: вона читає код лише під час старту, і без цього живий бот працюватиме по-старому.
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.
Пари призначає викладач. Одна людина в парі — 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 |
| Токен у Git | cd ~/app && git log -p | grep -c "BOT_TOKEN=" | 0, жодного рядка зі справжнім токеном | перевипустити токен у BotFather, а потім уже чистити історію (стара версія лишається в Git) |
| Автозапуск | systemctl --user show club-bot -p Restart | Restart=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 Team | Blue 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 ставить викладач.
Повернись до свого терміналу. Візьми готові скрипти уроку й подивись, як влаштовано бекап:
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.
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.timerdaemon-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.
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. Усе, що змінилося після останньої копії, буде втрачено, — тому копії й роблять щодня.
На занятті 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-------, знахідки немає, і це теж результат.
ss -ltn
systemctl --user show club-bot -p MemoryMax -p MemoryCurrent -p Restart -p NRestarts
free -mss -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 і відкриває нове з’єднання. Чужих процесів, служб і портів не чіпай.
Остання перевірка на міцність — найгрубіша зупинка, як у 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 — дивись «Якщо щось не працює».
Знахідка — слабке місце, яке ти справді перевірив: що зробив, що сталося, чому так і як закрив. Підходять знахідки із сьогоднішніх кроків, від однокласника й ті, що ти закрив раніше на курсі, — але лише справжні: якщо бот на 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 показує в кілька екранних рядків, але у файлі він один). Покажи файл однокласникові, з яким ви обмінювалися перевірками: його знахідки мають бути записані так, як він їх знайшов.
Цей крок робиш на своєму комп’ютері (не на сервері). У 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. Це крок для знайомства: пароль на нашому сервері лишається, а ключем ти вчишся входити так, як це роблять на справжніх серверах.
У Git ідуть код і BUGS.md. Журнал, копії бази й ~/restore-test — ні: це дані з іменами людей, і лежать вони поза ~/app, тож Git їх навіть не бачить. Файли таймера живуть у ~/.config/systemd/user/, теж поза репозиторієм, а їхні оригінали є в /opt/club/kurs/08/.
cd ~/app
git status --short
git add -A
git commit -m "Заняття 08: щит robust.py, журнал оновлень, бекап і відновлення, BUGS.md"
git push origin main
git push github maingit 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.5 | BUGS.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, не приватний). |
Кожне питання оцінюється відразу в браузері — це лишається так само. Ця кнопка лише надсилає підсумок учителю, щоб відповіді не зникли разом із закритою вкладкою.
Список критеріїв і що саме вони означають — у таблиці вище, «Що перевіряє автоматика». Тут — та сама перевірка, але прямо зараз, без очікування на найближчий прохід за розкладом.
tail -n 30 ~/logs/club-bot.log і tail -n 30 ~/logs/app.log) або в правах (ls -l, як у кроці 11); виправ саму причину, а не наслідок; перезапусти службу й повтори — тепер відповідь спокійна. Допиши в рядок у BUGS.md, як закрив, і попроси однокласника повторити свою перевірку.systemctl --user list-timers club-backup.timer
ls -l ~/backups
cd ~/app && python3 restore.pyLAST має стояти сьогоднішня ніч, у ~/backups — копія з часом 0330 в імені, а restore.py має відновити саме її. Якщо нової копії немає, подивись tail -n 20 ~/logs/club-backup.log.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 3awk '{print $4}' друкує четверте поле кожного рядка — команду, а sort | uniq -c | sort -rn рахує, скільки разів трапилась кожна, найчастіші першими. grep -c "ПОМИЛКА" — скільки разів щит упіймав помилку. Третя команда друкує номери чатів (третє поле), з яких писали найчастіше. Запиши в чернетку: які команди найпопулярніші, чи були помилки і від кого найбільше повідомлень.src.backup(), а не cp; чому відновлюють спершу в ~/restore-test; чому в app.log немає тексту повідомлень; чому в бота немає порту; як перевірити замок у чужого бота, нічого не зіпсувавши. На початку заняття 09 викладач спитає щось одне з цього.git add -A, git commit -m "Заняття 08: закрито знахідки однокласника", git push origin main і git push github main.Найчастіші причини, по черзі:
authorized_keys потрапив не весь рядок або він розірваний на два. Має бути один рядок, що починається з ssh-ed25519. Перевір: cat ~/.ssh/authorized_keys;ls -ld ~/.ssh ~/.ssh/authorized_keys має показати drwx------ і -rw-------. Виправ: chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys;.pub) — тоді рядок починається з -----BEGIN. Негайно видали цей рядок із сервера й створи нову пару ключів;-i — перевір шлях і що на Windows немає зайвих пробілів.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 готовоОстання команда показує помилку, не чекаючи служби. Що означає останній рядок:
ModuleNotFoundError: No module named 'robust' — robust.py не в ~/app: ls ~/app/robust.py, якщо файлу немає — крок 3;NameError: name 'robust' is not defined — пропущено import robust;IndentationError — відступи поплуталися. Над @robust.safe не має бути пробілів, а під ним — порожніх рядків;failed і start request repeated too quickly — systemd кілька разів поспіль не зміг підняти службу й перестав пробувати. Виправ код, потім systemctl --user reset-failed club-bot і systemctl --user restart club-bot.Повідомлення називає випадок, наприклад BAD: «дуже довге повідомлення»: відповідь 5012 символів… чи «message — не словник» валить бота: AttributeError.
відповідь … символів, а Telegram приймає до 4096 — немає щита: grep -n -B1 "def handle_update" ~/app/bot.py має показати @robust.safe; перевір ще, що вставлено import robust;валить бота: … — те саме: помилка вилетіла з handle_update, а щит її не зловив. Щит має стояти безпосередньо над def handle_update, а не над іншою функцією;у відповіді людині видно Traceback — у твоєму коді є except Exception as e: return str(e) або подібне. Повертай людині спокійний текст, а подробиці — у print() для журналу служби;handle_update має повертати текст або None — функція повертає словник чи число. Перетвори на рядок.Перед повторною перевіркою запусти python3 stress.py: це ті самі випадки, і видно, який саме падає.
python3 try_bot.py "/start"
tail -n 3 ~/logs/app.log
ls -ld ~/logs
grep -n "robust" ~/app/bot.pygrep нічого не знайшов — щит не підключено, крок 3;рядків з датою й часом менше, ніж оновлень — журнал пишеться не для кожного оновлення. Усі оновлення (стікер, редагування, довге) мають лишати рядок; з готовим robust.py це так, а власну версію звір з ним;у журналі видно текст повідомлення — ти дописав у рядок журналу сам текст. Лиши лише команду, довжину й номер чату;ls -ld ~/logs має показати drwxr-x--- з твоїм логіном. No such file or directory — створи теку (mkdir ~/logs) і перезапусти службу; служба має право писати лише в ~/app, ~/data, ~/logs, ~/backups і ~/www;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.logNo module named 'db' — ти не в ~/app; No module named 'yaml' — запусти /opt/club/venv/bin/python3 backup.py;~/.config/systemd/user/ немає club-backup.service і club-backup.timer — повтори копіювання з кроку 11, потім обов’язково systemctl --user daemon-reload. Без цього enable відповідає Unit file club-backup.timer does not exist;is-enabled каже disabled — таймер не ввімкнено: systemctl --user enable --now club-backup.timer;can't open file '/home/…/app/backup.py' — backup.py не скопійовано в ~/app (крок 10);найсвіжішому бекапу … год — таймер не спрацьовував: подивись стовпець LAST у list-timers, а поки розбираєшся, зроби свіжу копію: systemctl --user start club-backup.service.у ~/backups немає жодної копії — спершу python3 backup.py — так і зроби: спершу копія, потім відновлення;sqlite3.OperationalError: unable to open database file після python3 restore.py ~/backups/… — такого файлу немає. Подивись ls ~/backups і скопіюй ім’я точно (допомагає Tab: набери початок імені й натисни його — оболонка допише решту);у відновленій копії бракує таблиць: … — найсвіжіша копія старша за таблицю, яка вже є у справжній базі. Зроби нову копію й відновися з неї: python3 backup.py, потім python3 restore.py;у відновленій копії таблиця users порожня — скопійовано порожню базу. Порівняй sqlite3 ~/data/app.db "SELECT COUNT(*) FROM users" із тим, що друкує restore.py. Якщо у справжній базі люди є, перевір у project.yaml рядок file: app.db: саме звідти backup.py бере шлях до бази.systemctl --user status club-bot --no-pager
grep -E "^Restart" ~/.config/systemd/user/club-bot.serviceactivating — systemd ще відраховує свої три секунди або служба саме стартує: зачекай і повтори;failed — служба впала вже під час нового запуску: причина в журналі (tail -n 30 ~/logs/club-bot.log), найчастіше це помилка в коді, як у пункті про запуск вище. Виправ, потім systemctl --user reset-failed club-bot і systemctl --user start club-bot;inactive (dead) — службу зупинив розклад групи, це не поломка. Перевіряй під час заняття або ввечері;grep не знайшов Restart=always — це до викладача: файл служби налаштовує він.ls -l ~/app/BUGS.md
grep -c "^- " ~/app/BUGS.md
grep -n "^- " ~/app/BUGS.md | cut -c1-60No such file or directory — файл названо інакше (bugs.md, BUGS.txt) або він лежить не в ~/app. Знайди його (ls ~ ~/app | grep -i bugs) і перейменуй: mv ~/app/bugs.md ~/app/BUGS.md;*, •, — або дефіса без пробілу, чи одну знахідку розбито на кілька рядків. Кожна знахідка — один рядок, що починається з - ;- 500) не рахуються — та й нікому не допоможуть. Пиши, що зробив, що сталося, чому й як закрив. Третя команда показує початок кожного зарахованого рядка.