# Rozpoznanie robotow komunikatorow po naglowku User-Agent. # Aplikacja jest SPA, wiec bot nieuruchamiajacy JavaScriptu nie zobaczy tytulu # wydarzenia. Zadania takich robotow na /w/* kierujemy do backendu, ktory zwraca # maly dokument HTML z meta tagami. Zwykli uzytkownicy dostaja normalna aplikacje. map $http_user_agent $is_crawler { default 0; "~*facebookexternalhit" 1; "~*Twitterbot" 1; "~*LinkedInBot" 1; "~*WhatsApp" 1; "~*Slackbot" 1; "~*Discordbot" 1; "~*TelegramBot" 1; "~*Googlebot" 1; } server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # Nginx nie zdradza swojej wersji w naglowku Server ani na stronach bledow. server_tokens off; # Domyslny limit tresci zadania. Duze pliki (nagrania, materialy) maja # wlasny, znacznie wyzszy limit ustawiony punktowo na swoich sciezkach. # Bez tej linii limit /api/ (2100m) obejmowalby KAZDY endpoint, w tym # otwarte /api/public/** i /api/auth/login, wiec dowolny nieuwierzytelniony # nadawca moglby przyslac 2 GB i wyczerpac dysk albo pasmo. client_max_body_size 1m; # --- Naglowki bezpieczenstwa --- # Dokladane do KAZDEJ odpowiedzi (`always`, wiec takze przy bledach). # Aplikacja renderuje tresc HTML tworzona przez administratora # (tresc strony, banery, szablon certyfikatu) przez dangerouslySetInnerHTML. # HtmlSanitizer po stronie backendu jest pierwsza bariera, a CSP druga, # niezalezna warstwa: nawet gdyby cos przeszlo sanityzacje, przegladarka # nie wykona wstrzyknietego skryptu z obcego zrodla. add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "geolocation=(), microphone=(self), camera=(self)" always; # CSP dobrana do faktycznych zaleznosci aplikacji: # - script-src 'self': zadnego skryptu z obcej domeny, blokuje wstrzykniecia. # 'wasm-unsafe-eval' jest wymagane przez klienta LiveKit (dekodery WASM). # - style-src 'unsafe-inline': React, Tailwind, TipTap i LiveKit wstrzykuja # style inline; Google Fonts dostarcza arkusz z fonts.googleapis.com. # - font-src: pliki fontow IBM Plex z fonts.gstatic.com. # - img-src data:/blob:: obrazki edytora i miniatury; https: okladki lekcji. # - connect-src: API i WebSocket ida na wlasny host (ws:/wss:); LiveKit laczy # sie z wlasnym serwerem, wiec dopuszczamy dowolny wss:. # - frame-src: pokoj wideo (LiveKit/Jitsi) oraz wideo lekcji (external_video_url, # dowolny host http/https) osadzane sa w iframe. # - frame-ancestors 'self': panel osadza wlasne iframe, wiec 'self' zamiast # pelnego zakazu; chroni przed clickjackingiem z obcej strony. # - object-src 'none', base-uri 'self': domkniecie klasycznych wektorow. add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https:; media-src 'self' data: blob: https:; connect-src 'self' ws: wss:; frame-src 'self' https: http:; frame-ancestors 'self'; object-src 'none'; base-uri 'self'; form-action 'self'" always; # Interfejs jest po polsku, wiec kazda odpowiedz tekstowa musi jawnie # deklarowac UTF-8. Bez tego naglowek to samo "application/javascript", # a przegladarka zgaduje kodowanie pakietu, w ktorym siedza polskie znaki. # charset_types domyslnie nie obejmuje JavaScriptu, stad jawna lista. charset utf-8; charset_types text/html text/css text/plain text/xml application/javascript application/json image/svg+xml; # Strona webinaru. Robot dostaje zajawke z backendu, czlowiek aplikacje. # Rozgalezienie przez error_page zamiast if, bo "if" w kontekscie location # w polaczeniu z try_files zachowuje sie nieprzewidywalnie. location ~ ^/w/(?.+)$ { error_page 418 = @crawler_preview; recursive_error_pages on; if ($is_crawler) { return 418; } try_files $uri /index.html; } location @crawler_preview { proxy_pass http://backend:8080/api/preview/w/$webinar_slug; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # SPA: kazda sciezka nieodpowiadajaca plikowi trafia do index.html location / { try_files $uri $uri/ /index.html; } # Otwarte endpointy bez plikow: logowanie i webhooki. Male zadania JSON/XML, # wiec twardy limit 1 MB. Sa dostepne bez uwierzytelnienia, wiec to one sa # najtanszym celem do zalania duzym zadaniem. Prefiks `^~` wygrywa ze zwyklym # `/api/` i nie pozwala regexpom nizej przejac tych sciezek. location ^~ /api/auth/ { proxy_pass http://backend:8080/api/auth/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 1m; } location ^~ /api/webhooks/ { proxy_pass http://backend:8080/api/webhooks/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 1m; } # Wgrywanie i serwowanie DUZYCH plikow: nagrania i materialy webinaru. # Tylko te sciezki dostaja limit liczony w gigabajtach; reszta API zostaje # przy limicie posrednim, a otwarte endpointy przy 1 MB z poziomu server. # Regexp lapie zarowno wgranie (POST na /recordings, /materials), jak i # pobranie (/api/public/recordings/{id}, /api/public/materials/{id}). location ~ ^/api/(admin/webinars/[0-9]+/(recordings|materials)|public/(recordings|materials)/) { proxy_pass http://backend:8080$request_uri; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Nagranie potrafi miec 2 GB. Wartosc nie mniejsza niz RECORDING_MAX_SIZE_MB. client_max_body_size 2100m; # Wgranie i pobranie duzego pliku trwaja dluzej niz domyslne 60 s. proxy_read_timeout 3600s; proxy_send_timeout 3600s; # Pliki ida strumieniowo, bez buforowania calosci przed oddaniem. proxy_buffering off; proxy_request_buffering off; } # proxy do backendu, nazwa uslugi z docker compose location /api/ { proxy_pass http://backend:8080/api/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Posredni limit dla reszty API: obrazki tresci, branding, import CSV. # Wyzszy niz MEDIA_MAX_SIZE_MB (domyslnie 5), z zapasem na branding. client_max_body_size 25m; # Pobranie nagrania z tej trasy nie idzie (ma wlasna lokalizacje wyzej), # ale zostawiamy wydluzony odczyt na dluzsze operacje panelu. proxy_read_timeout 300s; proxy_send_timeout 300s; } # WebSocket ze STOMP do czatu, pytan i zgloszen do zabrania glosu location /ws { proxy_pass http://backend:8080/api/ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 3600s; } }