.env: форма стукає до твого сервераМодуль I · IT Skills (Cloud Services & Cyber Security) · тема 2, урок 1 з 2 · 120 хвилин
Досі твій сайт лише віддавав сторінки. Сьогодні він уперше почне приймати дані. Від Google, автоматично, щойно хтось надішле анкету. Це і є вебхук.
Ти збереш ланцюг із п’яти ланок. Дані з чужої форми самі приїжджатимуть у твій файл. І перший у житті секрет ляже так, як його кладуть дорослі.
Слова «вебхук», «токен», .env і «JSONL» поки
нічого не означають — розберемо їх по черзі.
| Етап | Хв | Що робимо |
|---|---|---|
| Вхід і мотивація | 5 | Дивимося ланцюг «форма → твій файл» цілком |
| Вебхук проти опитування | 10 | Хто кого питає; часова шкала з вісьмома марними запитами |
| POST і коди відповіді | 15 | Анатомія запиту, 200, 401, 403, 405, 422, інспектор запиту |
Токен і ~/.env | 15 | openssl rand, права 600, сканер секретів |
| Практика: форма і токен | 20 | Google Form, Script Properties, ~/.env по SSH |
Практика: inbox.php | 25 | Шість перевірок, README-контракт, заливання по FTP |
Практика: три тести curl | 15 | 401, 403, 200 і рівно +1 рядок у leads.jsonl |
| Автоперевірка й підсумок | 15 | Чотири 🔒-критерії доводимо до зеленого, ДЗ |
| Разом | 120 |
У кінці уроку в тебе працює:
~/www/api/inbox.php — приймач, який перевіряє токен і коди відповіді;~/.env із правами 600 — там і тільки там лежить токен;~/data/leads.jsonl — журнал анкет поза веб-корінням.| Файл | Де живе | Як туди потрапляє |
|---|---|---|
| inbox.php | ~/www/api/ | пишеш у себе на комп’ютері, заливаєш по FTP |
| README.md | ~/www/api/ | пишеш у себе на комп’ютері, заливаєш по FTP |
| .env | ~/ (домашня тека) | створюєш по SSH просто на сервері — по FTP ніколи |
| leads.jsonl | ~/data/ | створює й дописує сам inbox.php |
.php, .md,
.html, .json. Секрети й приватні дані не їдуть по FTP узагалі —
.env ти набираєш у SSH-сесії, а leads.jsonl сервер пише собі сам.check 02 — за номером
теми, а не уроку. Урок третій, тема друга. Не шукай lesson-03.Одна зміна напрямку — і зникає ціла категорія роботи
Є два способи дізнатися, що прийшла нова анкета.
Опитування (polling). Твоя програма щохвилини питає Google: «є щось нове?» У 99 випадках зі 100 відповідь — «ні». Ти витрачаєш запити, трафік і ліміти. А новина все одно приходить із запізненням до хвилини.
Вебхук (webhook). Ти один раз кажеш Google свою адресу. Далі мовчиш. Хтось надсилає форму — і Google сам стукає на цю адресу. Дані він приносить у тілі запиту. Нуль зайвих запитів, затримка — секунди.
Стукати можна лише в двері, які видно з вулиці. Твій ноутбук сидить
за домашнім роутером. З інтернету його не знайти: адреси, за якою можна
прийти, у нього просто немає. Тому в тебе є сабдомен
91.219.61.4/s/<логін>. Це і є твої двері.
Три вимоги до адреси, яку ти віддаєш чужому сервісу:
localhost і не 192.168.x.x;Ти щохвилини оновлюєш вкладку з таблицею відповідей і дивишся, чи прийшла нова анкета. Що саме змінить вебхук у цій картині?
Ти вписав у Apps Script адресу http://localhost/api/inbox.php.
У журналі виконань — помилка з’єднання. Чому?
Метод, шлях, заголовки, порожній рядок, тіло. Чотири частини — і кожна щось означає
HTTP-запит — це звичайний текст, який їде по мережі. Відкриваєш сторінку —
браузер шле GET: «дай мені це». Apps Script надсилає анкету —
він шле POST: «на, візьми це й зроби щось».
X-Auth-Token — власна вигадка твого контракту:
префікс X- традиційно означає «нестандартний, домовлений між нами».| Куди покласти токен | Що з ним стається |
|---|---|
| ?token=… в URL | потрапляє в логи доступу nginx, в історію браузера, у заголовок Referer — тобто зберігається у трьох місцях відразу |
| у тілі JSON | працює, але щоб дістати токен, треба спершу розібрати тіло — а тіло приходить від невідомо кого |
| у заголовку | перевіряється до розбору тіла, у стандартні логи не пишеться, шифрується разом із рештою запиту по HTTPS |
Ти виклав адресу /api/inbox.php у публічному README на GitHub
і поки не поставив жодної перевірки. Що станеться раніше за перший
справжній запис?
Код — це не «помилка». Це коротка інструкція, що робити далі
Той, хто до тебе стукає, — програма. Вона не читає українською. Єдине, що вона розуміє, — тризначне число у відповіді. Від нього залежить, повторить вона запит чи назавжди його викине.
| Код | Назва | Що це означає для того, хто стукає |
|---|---|---|
| 200 | OK | прийнято й записано. Повторювати не треба |
| 401 | Unauthorized | ти взагалі не показав ключа. Додай заголовок і спробуй ще раз |
| 403 | Forbidden | ключ показав, але не той. Повторювати з тим самим ключем безглуздо |
| 422 | Unprocessable Content | запит зрозумілий, ключ правильний, але вміст не годиться: не JSON або бракує поля. Виправ дані |
| 429 | Too Many Requests | занадто часто. Зачекай і повтори — скільки саме, скаже заголовок Retry-After |
Ще один код побачиш майже одразу: 405 Method Not Allowed. Він
прилетить, щойно ти відкриєш /api/inbox.php у браузері. Браузер
шле GET, а твій приймач бере лише POST.
422 звузиться
Сьогодні приймач віддає 422 у двох випадках. Перший — тіло не
розібралося як JSON. Другий — бракує обов’язкового поля. На уроці 4 ми їх
розділимо: нерозбірне тіло дістане 400, а 422
лишиться для правильного JSON із поганими значеннями. Тоді ж ти оновиш
README.md одним рядком.Твій обробник на будь-яку проблему відповідає 200 ok — «щоб
Apps Script не лаявся». Чим це закінчиться?
Склади запит — і подивись, на якому саме кроці він помре
Нижче — модель твого обробника. Обираєш, що покласти в запит, і тиснеш
«Надіслати». Далі покроково видно, як приймач його перевіряє. Який код
повернеться. Що потрапить у лог. І чи додасться рядок
у leads.jsonl.
Інспектор — модель, а не справжня мережа: він показує ту саму
послідовність перевірок, що й код inbox.php нижче. Числа в рядку
логу (IP, довжина, час) — ілюстративні.
Apps Script шле contentType: 'application/x-www-form-urlencoded',
а приймач чекає JSON. Токен при цьому правильний. Що поверне приймач
і що з’явиться в leads.jsonl?
Твоя адреса — не таємниця. Таємниця — ключ до неї
Типове заперечення: «адресу знаю тільки я, навіщо перевірка?» Адресу знаєш
не тільки ти. Публічні адреси знаходять автоматично. За списками виданих
сертифікатів HTTPS. За чужими логами й посиланнями. Просто перебором типових
шляхів: /api/, /webhook.php, /inbox.php.
Це роблять цілодобово й не проти тебе особисто — сканують усе підряд.
Що буває з ендпойнтом без перевірки:
leads.jsonl лягають тисячі порожніх або рекламних записів,
і справжню анкету ти вже не знайдеш;Найпростіший захист — спільний секрет (shared secret). Обидві сторони
знають один рядок. Той, хто стукає, кладе його в заголовок. Це не криптографія
й не підпис. Хтось підгляне цей рядок — і стукатиме замість тебе. Тому це
перший рівень, а не останній. Справжній підпис HMAC
з міткою часу ти зробиш на уроці 12.
hash_equals()
Звичайне == порівнює рядки посимвольно й зупиняється на першій
розбіжності — тобто відповідає трохи швидше, коли перші символи вгадані.
Це називають атакою по часу. hash_equals() витрачає однаковий час
на будь-яке значення.Токен має бути випадковим і довгим. Не «myschool2026», не дата народження, не назва класу. Генерує його машина:
-hex 24 — це 24 випадкові байти, записані як 48 шістнадцяткових
символів. Підібрати такий рядок перебором неможливо. Варіантів 2 у степені
192 — число з 58 цифр.
Однокласник каже: «я поставив токен 10b2026, його ніхто не
вгадає, бо про наш клас ніхто не знає». Що з цим не так?
.env: чому не в кодіТри різні шляхи, якими код витікає — і жоден із них не про злам
Написати $token = "a7f3c9e2…"; просто в inbox.php
здається безпечним: цей файл же на сервері. Проблема в тому, що код не лишається
на сервері.
Ти покажеш файл однокласнику, попросиш допомоги в чаті, скинеш скрін учителю, вставиш у AI-редактор. Токен їде разом із кодом у кожну копію. І жодну з цих копій ти вже не контролюєш.
Комміт — це запис назавжди. Ти видалиш рядок наступним коммітом, а старий
лишиться в історії. Команда git log -p покаже його будь-кому,
хто має репозиторій. На GitHub такі рядки знаходять автоматично. Секрет,
залитий у публічний репозиторій, зазвичай починають використовувати
за хвилини, а не за дні.
Це найгірший випадок. Частина файлів у ~/www/ сервером
не виконується: .js, .json, .txt,
.env, .bak. Такий файл віддають кожному, хто
попросить. Ламати нічого не треба — досить знати ім’я.
index.html». Файл усередині
~/www/ віддається по HTTP за замовчуванням: щоб його прочитати,
достатньо знати ім’я.Ти поклав токен у ~/www/config.js поруч із
index.html. Хто його вже прочитав?
600: що це насправді означаєНа сервері живуть три класи, разом 63 учні. Файл із правами 644
(-rw-r--r--) читає будь-хто з них однією командою
cat. Для секрету потрібне 600.
| Права | Символьно | Хто читає твій .env |
|---|---|---|
| 600 | -rw------- | тільки ти. Це єдиний правильний варіант |
| 640 | -rw-r----- | ти й уся твоя група |
| 644 | -rw-r--r-- | кожен із 64 акаунтів сервера |
| 666 | -rw-rw-rw- | кожен із 64 акаунтів, і ще й переписати може |
Два нижні рядки — це блокуючий критерій env_exists. Поки він
червоний, роботу не приймуть. Спроби не обмежені: постав 600
і натисни перевірку ще раз.
chmod стоїть до запису
Файл створюється з правами за замовчуванням, і між створенням і
chmod є вікно, коли він читається всіма. Секунда — теж вікно.
Тому спершу закриваємо, потім пишемо.Ти зробив ls -l ~/.env і бачиш -rw-r--r--.
На сервері зараз 64 акаунти трьох класів. Що це означає практично?
.gitignore і .env.exampleЯкщо твій проєкт лежить у git, .env має бути в
.gitignore до першого комміту. Файл
.gitignore — це список того, що git ігнорує. Сам він
у репозиторій їде, і в цьому сенс. Правило спрацює в кожного, хто клонує проєкт.
Разом із ним у репозиторій кладуть .env.example — той самий
файл, але без значень. Він відповідає на питання «які змінні потрібні,
щоб це запустилося», і не розкриває жодної.
.gitignore не рятує
Якщо .env уже потрапив хоча б в один комміт, він лишається в
історії назавжди, і додавання правила нічого не змінює. Єдина правильна
реакція — вважати токен скомпрометованим і перевипустити його:
згенерувати новий, замінити в ~/.env і в налаштуваннях Apps Script.Ти додав .env у .gitignore — але вже після того,
як один раз закоммітив його. Достатньо?
Навіщо тримати в репозиторії .env.example, якщо сам
.env туди не потрапляє?
Є два способи віддати програмі налаштування: покласти у файл конфігу або передати змінною оточення. У них різні сильні сторони.
| Ознака | Файл конфігу | Змінна оточення |
|---|---|---|
| Де живе значення | у файлі на диску | у пам’яті процесу, який запустив програму |
| Ризик витоку | файл можна випадково залити в git або в www | у файли не потрапляє, але видно в /proc і в списку процесів на деяких системах |
| Різні значення для тесту й бою | треба два файли | міняється одним рядком запуску |
| Зручність для нас сьогодні | працює і без доступу до налаштувань сервера | треба керувати конфігурацією PHP-FPM, а sudo в тебе немає |
.env — компроміс між ними. Це файл, який виглядає як
список змінних оточення. Ідея прийшла з методології «Дванадцять факторів».
Правило там просте: налаштування живуть окремо від коду. Код однаковий
у всіх, а налаштування в кожного свої. Ми беремо це правило й робимо його
найпростішим доступним способом.
Встав свій код — сторінка підсвітить рядки, які не можна нікуди віддавати
Такі самі інструменти працюють у GitHub і в багатьох CI. Змісту коду вони не розуміють. Вони шукають форму: довгі випадкові рядки й присвоєння змінним із підозрілими іменами. Тому й помиляються в обидва боки. Іноді підсвітять невинне, іноді пропустять справжнє. Останнє слово завжди за тобою.
Сканер підсвітив у твоєму inbox.php рядок
$fallback = "a7f3c9e2…24f8e"; — ти вписав його «на випадок,
якщо .env не прочитається». Чим це небезпечно саме тут?
Два рішення, які приймають один раз і потім не переробляють
JSONL (JSON Lines) — це файл, у якому один рядок = один самодостатній JSON-об’єкт. Ніякої обгортки, ніякого масиву навколо, ніякої коми в кінці рядка.
| Ситуація | CSV | JSONL |
|---|---|---|
| У повідомленні є кома | треба брати значення в лапки, інакше рядок поїде по колонках | кома всередині рядка нічого не ламає |
| У повідомленні є перенос рядка | один запис займає кілька фізичних рядків — wc -l уже бреше | переноси екрануються як \n, запис лишається одним рядком |
| Додали нове поле | міняється заголовок, старі рядки стають несумісними | у нових рядках просто з’являється новий ключ |
| Дописати запис | можна, але формат не гарантує коректності | дописування — штатний режим формату |
| Читати частинами | треба тримати стан парсера | читаєш по рядку, кожен розбирається окремо |
| Відкрити в Excel | працює одразу | потрібна конвертація |
А чому не один великий leads.json із масивом? Щоб додати запис
у масив, треба прочитати весь файл, розібрати його, дописати елемент
і переписати файл цілком. Два запити одночасно — і половина даних зникла.
У JSONL дописування — одна операція. Система робить її атомарно: відкриває
файл у режимі append і додає рядок у кінець.
Ти пишеш анкети в leads.csv. Приходить повідомлення
«Привіт, я з 10-Б, хочу в гурток, бажано вівторок» — з комами й переносом
рядка. Що станеться зі звітом, який рахує рядки?
~/data/ проти ~/www/data/Nginx віддає по HTTP лише те, що лежить усередині DocumentRoot.
Для тебе це тека ~/www/. Усе, що поза нею, з інтернету недосяжне
в принципі. Не тому що заборонено. А тому що з адреси в браузері туди просто
не веде жоден шлях.
www/
лежить те, що ти готовий показати всьому світу. Усе інше — рівнем вище.leads.jsonl кладуть у ~/www/data/, «щоб зручно було
переглядати в браузері». Зручно стає всім: одне посилання — і всі анкети
однокласників у чужих руках. Це блокуючий критерій data_outside:
поки він червоний, роботу не приймуть.Ти хочеш дивитися накопичені анкети з телефона. Найпростіше — перенести
leads.jsonl у ~/www/data/. Який наслідок і що
робити натомість?
Три файли. Кожен рядок ти маєш уміти пояснити вголос
~/www/api/inbox.phpКроки в коді пронумеровані так само, як гілки дерева рішень вище.
Що тут варто прочитати уважно:
dirname(__DIR__, 2) — шлях до домашньої теки рахується від
самого файлу. Не /home/koval.d.10b жорстко: скопіюєш код
однокласнику — він запрацює й у нього;php://input — сире тіло запиту. $_POST тут
порожній, бо PHP наповнює його лише для форм, а не для JSON;FILE_APPEND | LOCK_EX — дописати в кінець і на час запису
замкнути файл, щоб два одночасні запити не переплелися;mb_substr — обрізаємо довжину одразу. Це не валідація, це
межа: повну валідацію ти зробиш наступного уроку.~/data/ має існувати
file_put_contents створює файл, але не теку. Якщо
~/data/ немає, запис мовчки провалиться, а відповідь усе одно буде
200. Створи теку заздалегідь: mkdir -p ~/data && chmod 700 ~/data.У Google теж є свій «.env» — Script Properties. Токен
кладеш туди через меню налаштувань проєкту. У коді лишається саме звернення
до нього.
e.namedValues — це словник, у якому ключ дорівнює тексту питання
у формі. Скопіюй назву з форми, а не набирай руками. Апостроф у слові
«Ім’я» теж має бути той самий. Інакше v['Ім’я'] поверне
undefined, поле поїде порожнім, а приймач відповість
422 missing_field.onFormSubmit сама по собі — «простий» тригер, а
простим тригерам заборонено звертатися до сервісів, що потребують дозволу, — і
UrlFetchApp саме такий. Тригер треба створити встановлюваним:
у редакторі скрипта «Тригери» → «Додати тригер» → подія «При надсиланні форми»,
і дати дозвіл, коли Google його запитає.~/www/api/README.md — контрактКонтракт — це відповідь на питання «як до тебе стукати». Без нього твій ендпойнт не зможе використати навіть ти сам за два тижні.
Запускай їх у SSH-сесії — там справжній curl. Заміни
<логін> і <токен> на свої.
curl — це не curl
У Windows PowerShell 5.1 слово curl — псевдонім
Invoke-WebRequest, і всі ключі -X, -H,
-d там означають зовсім інше. Якщо запускаєш у PowerShell, пиши
curl.exe повністю. У Git Bash, macOS, Linux і в SSH-сесії на
сервері — звичайний curl.Ти прибрав з inbox.php рядок із hash_equals і
поставив if ($token != $got). Функціонально нічого не
змінилося: правильний токен проходить, чужий — ні. Що ти втратив?
Кроки 1—2 — у браузері в Google, 3—6 — у SSH-сесії, 7—9 — у себе на комп’ютері, 10—16 — знову сервер і форма. Галочки зберігаються, сторінку можна закрити.
~/.env — набирається в SSH-сесії просто на сервері.
~/data/leads.jsonl — створює сам приймач. Якщо ти бачиш
.env у вікні FileZilla серед файлів для завантаження — зупинись
і видали його звідти.Чекер стукає до тебе тричі: без токена, з чужим токеном і з правильним. Далі дивиться права й місце файлів. Спроби не обмежені.
Демонстраційний режим: результат згенеровано для показу. На
сервері ця кнопка викликає POST /api/check, який запускає
check 02 від імені учня.
200;tail -n 1 ~/data/leads.jsonl — останній
рядок збігається з тим, що ти щойно ввів у форму;ls -l ~/.env читаєш уголос: хто власник і які три групи прав;Це те, що бачить учитель, коли виставляє оцінку.
| Складник | Вага | Результат |
|---|
www з правами
600, а в коді лишається лише звернення до змінної;DocumentRoot, інакше їх віддає веб-сервер
кожному охочому;~/.env створено з правами 644 — блокуючий
критерій env_exists червоний;application/x-www-form-urlencoded, а
приймач чекає JSON — тихий 422 і жодного запису;leads.jsonl покладено в ~/www/data/ «щоб зручно
дивитися» — блокуючий критерій data_outside червоний;~/data/ не створили — приймач відповідає
200, а файл не з’являється.Допиши ~/www/api/README.md. Опиши всі коди відповіді
свого приймача. І що кожен означає для того, хто стукає. Не «401 — помилка»,
а «401 — заголовка X-Auth-Token немає взагалі; треба дослати
заголовок, повторювати без нього безглуздо».
Додай наприкінці абзац-гіпотезу до наступного уроку. Що станеться,
якщо в поле «Ім’я» вставити <script>alert(1)</script>,
а твоя сторінка потім покаже це ім’я?
Усе на цій сторінці — зі стандартів і офіційної документації. Перевіряти дозволено й корисно.
Retry-After.
rfc-editor.org/rfc/rfc6585UrlFetchApp.fetch — параметри
method, contentType, headers,
payload, muteHttpExceptions.
developers.google.com/apps-script/reference/url-fetchonFormSubmit не має права на UrlFetchApp.
developers.google.com/apps-script/guides/triggers/installablePropertiesService — сховище властивостей
скрипта, «.env» на боці Google.
developers.google.com/apps-script/guides/propertiesparse_ini_file, hash_equals,
file_put_contents — усе, що робить приймач.
parse-ini-file,
hash-equals,
file-put-contentsgitignore, документація Git — синтаксис і чому пізнє
правило не прибирає файл з історії.
git-scm.com/docs/gitignorechmod і вісімкові права — man-сторінка.
man7.org/chmodroot і межа веб-кореня — чому файл
поза DocumentRoot недосяжний по HTTP.
nginx.org/ngx_http_core_module#roothash_equals.
en.wikipedia.org/wiki/Timing_attackПовний список із поясненнями, що звідки взято, —
у файлі urok-03-джерела.md поруч із цією сторінкою.