# Розгортання Веб-інфраструктури Radicle на прикладі оверлейних мереж У попередньому гайді серії про децентралізований Git-хостинг Radicle, було розглянуто приклад налаштування публічного сіда для поширення персональних репозиторіїв в оверлейному режимі з політикою "Selective": => radicle-multi-network-seed-deployment.gmi Розгортання сіда Radicle в мульти-мережному середовищі Цього разу, опишу особистий досвід розгортання публічного Веб-інтерфейсу на його основі, для користувачів оверлейних IPv6 мереж Yggdrasil і Mycelium. Мотивація - зробити сідуючий сервер доступним для локальних користувачів, які бажають переглядати репозиторії "всередині" оверлейної мережі та мати можливість забирати код як з `rad clone` так і через шлюз HTTP звичною командою `git clone`. Веб-інфраструктура Radicle ділиться на дві основні частини: сервер JSON/API (за яку відповідає пакунок `radicle-httpd`) і статичний асинхронний клієнт (на базі технологій HTML і JavaScript). Обидва рішення є частиною репозиторію `radicle-explorer`. ## Отримання початкового коду У попередньому гайді, на сервері вже було створено користувача `radicle`, тож спочатку залогінимось від нього: ``` bash su radicle ``` Оскільки на моєму сервері немає Інтернет-інтерфейсу як такого, але вже є підключений до глобальної мережі (засобами Tor over Yggdrasil) `radicle-node`, я буду тягнути вихідний код засобами команди `rad` а не `git` (для якого в моєму випадку знадобився б вихідний проксі). Це за одно дозволить звикнути до нової обгортки і скористатись перевагами пірингового обміну без прив'язки до конкретної мережі: ``` bash rad clone rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5 radicle-explorer ``` Якщо таки збираєте клієнт без локального вузла Radicle: ``` bash git clone https://iris.radicle.xyz/z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git radicle-explorer ``` ## Сервер JSON/API (radicle-httpd) Інструкції з розгортання також описані в офіційній документації: => https://radicle.xyz/guides/seeder#running-the-http-daemon Radicle Seeder Guide: Running the HTTP Daemon Якщо коротко, то робимо наступне: ``` bash cd radicle-explorer/radicle-httpd cargo build --release ``` Копіюємо отриманий бінарник `target/release/radicle-httpd` до `/usr/local/bin` на сервері і переконуємось що користувач `radicle` має відповідні права на його виконання: ``` bash sudo chown radicle:radicle /usr/local/bin/radicle-httpd sudo chmod +x /usr/local/bin/radicle-httpd ``` ### Системний сервіс Сервіс я оголосив на локальному інтерфейсі `[::1]` (для IPv4 - це може бути `127.0.0.1`) з портом `8788` і подальшим проксуванням через Nginx з публічних IP відповідних мереж. ``` /etc/systemd/system/radicle-httpd.service [Unit] Description=Radicle HTTP Daemon After=network.target network-online.target Requires=network-online.target [Service] User=radicle Group=radicle ExecStart=/usr/local/bin/radicle-httpd --listen [::1]:8788 Environment=RAD_HOME=/home/radicle/.radicle RUST_BACKTRACE=1 RUST_LOG=info NO_COLOR=1 KillMode=process Restart=always RestartSec=1 StandardOutput=file:///home/radicle/httpd-debug.log StandardError=file:///home/radicle/httpd-error.log [Install] WantedBy=multi-user.target ``` Додаємо сервіс до авто-запуску зі стартом системи і запускаємо сервер: ``` bash sudo systemctl enable --now radicle-httpd ``` => https://seed.radicle.xyz/raw/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/570a7eb141b6ba001713c46345d79b6fead1ca15/systemd/radicle-httpd.service Приклад конфігурації systemd в офіційному репозиторії ### Nginx ``` /etc/nginx/sites-available/default server { listen [202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148]:8788; listen [505:6847:c778:61a1:5c6d:e802:d291:8191]:8788; # Поки не визначився # server_name _; access_log /var/log/nginx/radicle.access.log; location / { proxy_pass http://[::1]:8788; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` * для мереж Yggdrasil і Mycelium - HTTPs звичайно не використовується, тому приклад налаштування спрощено Застосовуємо оновлення конфігурації: ``` bash systemctl reload nginx ``` ### Фаєрвол ``` sudo ufw allow from 0200::/7 to 202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148 port 8788 proto tcp comment 'radicle-httpd' sudo ufw allow from 0400::/7 to 505:6847:c778:61a1:5c6d:e802:d291:8191 port 8788 proto tcp comment 'radicle-httpd' ``` ### Тестування бекенду В рамках цього прикладу, перевірити роботу JSON/API, можна за адресами: => http://[202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148]:8788 => http://[505:6847:c778:61a1:5c6d:e802:d291:8191]:8788 Якщо встановлено Alfis DNS: => http://ygg.ua.srv:8788 => http://myc.ua.srv:8788 По аналогії, на бекенд можна легко причепити тунелі I2P і Tor. ## Клієнт (radicle-explorer) По суті, це статичний Веб-компонент на базі HTML/JavaScript, що звертається до вказаного в його налаштуваннях сервера. Оптимізована статика збирається засобами пакетного менеджера `npm`, після чого отримані файли з теки `build` копіюються до публічного простору, наприклад Nginx: `/var/www/radicle`. ### Налаштування підключення до бекенду Перед тим, як збирати оптимізований білд, важливо спочатку вказати актуальні налаштування підключення до серверів JSON/API - власних або сторонніх. Робиться це у файлі `config/default.json`. Після збірки, ці налаштування будуть "вбудовані" в компонент `build/assets/components-xxx.js` і при наступних оновленнях конфігурації, потрібно буде перезбиратись. Щоб уникнути цієї незручності, можна використовувати опцію динамічних налаштувань `VITE_RUNTIME_CONFIG=true`, про яку детальніше описано у розділі "Компіляція". У своїй конфігурації, поки використовую два інтерфейси: Yggdrasil і Mycelium свого сіда. По аналогії, до масиву об'єктів `preferredSeeds` додаються й альтернативні DNS, тунелі I2P або приховані сервіси Tor. Важливим є той факт, що поточна версія `radicle-explorer` ніяк не проксує через бекенд `preferredSeeds` і тому якщо в клієнта не встановлено роутер Mycelium, то перебуваючи на хості Yggdrasil - він не зможе з меню `0200::/7` відкрити сід `0400::/7`. Навожу приклад міксованого з'єднання, але рекомендую користуватись одним сімейством адрес для одного фронтенду. ``` config/default.json { "nodes": { "fallbackPublicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path", "requiredApiVersion": "~0.18.0", "defaultHttpdPort": 443, "defaultLocalHttpdPort": 8080, "defaultHttpdScheme": "http" }, "source": { "commitsPerPage": 30 }, "supportWebsite": "https://radicle.zulipchat.com", "deploymentId": null, "preferredSeeds": [ { "hostname": "[202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148]", "port": 8788, "scheme": "http" }, { "hostname": "ygg.ua.srv", "port": 8788, "scheme": "http" }, { "hostname": "[505:6847:c778:61a1:5c6d:e802:d291:8191]", "port": 8788, "scheme": "http" }, { "hostname": "myc.ua.srv", "port": 8788, "scheme": "http" } ] } ``` Стандартні поля секції `nodes` - свідомо не змінюю (окрім встановлення стандартної схеми `http`), бо в коді клієнта є такі упороті моменти: ``` radicle-explorer/src/views/repos/Header/CloneButton.svelte $: portFragment = baseUrl.scheme === config.nodes.defaultHttpdScheme && baseUrl.port === config.nodes.defaultHttpdPort ? "" : `:${baseUrl.port}`; ``` * через що може не працювати внутрішня навігація по сідам а також приклади команд типу `git clone`, виключаючи з URL не типовий для HTTP/80 порт (`8788`) якщо той використовується ### Зауваження щодо HTTP Якщо відкрити Веб-інтерфейс Radicle на віддаленому (не `localhost`) сервері з протоколом HTTP, то при копіюванні адрес репозиторію до буферу - в консолі браузера буде помилка: > Uncaught (in promise) TypeError: can't access property "writeText", navigator.clipboard is undefined Вона означає, що сучасна політика браузера блокує функціональність буферу копіювання для не захищеного протоколу HTTP, за виключенням `localhost` або при ручному додаванні такого виключення через `about:config`. Звісно, для продакшну це не варіант, бо переважна більшість користувачів через захищену природу оверлейних мереж, не користуються HTTPs і вважають таке явище - швидше не правильно налаштованим Nginx, аніж фічею. Отже, якщо не плануєте "силою" заганяти юзерів на HTTPs (з само-підписаним сертифікатом) то рішення я знайшов тільки у застосуванні патчу `src/lib/utils.ts`, із заміною його функції `toClipboard` на "legacy-fallback" через `copyToClipboard`: ``` src/lib/utils.ts async function copyToClipboard(text: string) { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); } else { const textArea = document.createElement("textarea"); textArea.value = text; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); } catch (err) { console.error('Unable to copy', err); } document.body.removeChild(textArea); } } export async function toClipboard(text: string): Promise { await copyToClipboard(text); } ``` * Firefox 148 - все ще підтримує `document.execCommand('copy')` ### Компіляція Після завершення конфігурації, на локальній машині збираємо оптимізовану статику і для зручності передачі на сервер, запаковуємо вміст `build` до архіву `radicle-explorer.tar.gz`: ``` bash cd radicle-explorer npm install VITE_RUNTIME_CONFIG=true npm run build tar -czvf radicle-explorer.tar.gz -C build . ``` * рекомендую збирати з підтримкою динамічного `config.json` (`VITE_RUNTIME_CONFIG=true`), інакше після кожної зміни конфігурації, доведеться заново перезбирати весь білд і перезаливати його на сервер; детальніше про це написано [тут](https://app.radicle.xyz/nodes/iris.radicle.xyz/rad%3Az4V1sjrXqjvFdnCUbxPFqd5p4DtH5/tree/README.md#run-time-configuration). ### Встановлення Отриманий архів `radicle.tar.gz` копіюємо на сервер до якоїсь тимчасової теки і розпаковуємо вміст архіву за призначенням `/var/www/radicle`, оновивши за одно права для доступу служби Nginx: ``` bash sudo mkdir -p /var/www/radicle sudo tar -xzvf /tmp/radicle-explorer.tar.gz -C /var/www/radicle sudo rm /tmp/radicle-explorer.tar.gz sudo chown www-data:www-data /var/www/radicle ``` ### Nginx Тут все просто: вказуємо актуальний шлях до статики `/var/www/radicle` з адресацією усіх запитів на `index.html`: ``` /etc/nginx/sites-available/default server { listen [202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148]:8780; listen [505:6847:c778:61a1:5c6d:e802:d291:8191]:8780; # Ще не визначився # server_name _; access_log /var/log/nginx/radicle.access.log; root /var/www/radicle; location / { try_files $uri $uri/ /index.html; } # Якщо збірка з `VITE_RUNTIME_CONFIG=true` # # location = /config.json { # root /; # try_files /etc/radicle-explorer/config.json /usr/share/radicle-explorer/config.json =404; # } # Цей блок є опціональним і дозволяє публікувати `radicle-httpd` і `radicle-explorer` # на одному інтерфейсі одночасно (на прикладі це може спільний порт `8780`) # таким чином, індексна сторінка API буде заміщена користувацьким Веб-інтерфейсом # # location ~ "^/(raw|api|[a-zA-Z0-9]{28,29}(\.git)?)(/|$)" { # proxy_pass http://[::1]:8788; # # proxy_http_version 1.1; # proxy_set_header Connection ""; # # proxy_set_header Host $host; # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # proxy_set_header X-Forwarded-Proto $scheme; # } } ``` Застосовуємо оновлення конфігурації: ``` bash systemctl reload nginx ``` ### Фаєрвол ``` bash sudo ufw allow from 0200::/7 to 202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148 port 8780 proto tcp comment 'radicle-explorer' sudo ufw allow from 0400::/7 to 505:6847:c778:61a1:5c6d:e802:d291:8191 port 8780 proto tcp comment 'radicle-explorer' ``` ### Тестування фронтенду В рамках цього прикладу, перевірити роботу можна за адресами: => http://[202:68d0:f0d5:b88d:1d1a:555e:2f6b:3148]:8780 => http://[505:6847:c778:61a1:5c6d:e802:d291:8191]:8780 Якщо встановлено Alfis DNS: => http://ygg.ua.srv:8780 => http://myc.ua.srv:8780