Řešení problémů s CORS v DIY projektech
Dnes je síťová komunikace jedním z klíčových aspektů, které umožňují interakci mezi elektronickými zařízeními a uživateli. Využíváme ji na každém kroku – od profesionálních IoT (Internet of Things) zařízení až po různé DIY (Do-it-Yourself) projekty, které si vytvářejí nadšenci. Díky dostupnosti moderních nástrojů a technologií mohou i lidé bez hlubokých odborných znalostí navrhnout a implementovat vlastní síťová řešení. Mnozí se tak pouštějí do vytváření domácí automatizace, chytrých zařízení nebo senzorů, které komunikují prostřednictvím internetu.
Není však výjimkou, že se právě takové DIY projekty najednou dostanou do problémů. Mnoho těchto projektů přestane fungovat, aniž by jejich tvůrci rozuměli tomu, proč. Po pečlivém prozkoumání mohou zjistit, že za údajnou „poruchou“ stojí tzv. pravidla CORS. Tato pravidla, pokud nebudou správně nakonfigurována, totiž mohou doslova „odstřihnout“ síťovou komunikaci mezi webovými aplikacemi a servery nebo jinými zařízeními. Tato omezení, i když mají na první pohled chránit bezpečnost, může být pro neprofesionální projekty nečekanou a těžko pochopitelnou překážkou.
Co to CORS znamená?
CORS (Cross-Origin Resource Sharing) je soubor pravidel, která umožňují nebo omezují přístup k určitým zdrojům na serveru z jiných domén, než je ta, ze které pochází samotná aplikace. Tento mechanismus slouží k ochraně uživatelských dat před neautorizovanými přístupy a útoky, jako je například CSRF (Cross-Site Request Forgery).
Jak funguje CSRF útok
CSRF je nebezpečný útok, protože dokáže vykonat neautorizované akce v rámci přihlášené relace uživatele, aniž by si toho uživatel všiml. CSRF využívá skutečnosti, že webové prohlížeče automaticky posílají uložené cookies (například přihlašovací údaje) při každé komunikaci s webem, na který uživatel přistupuje, i když uživatel tento požadavek neprovádí přímo.
Postup útoku:
- Uživatel je přihlášený na webovou aplikaci (například na bankovní účet nebo e-shop).
- Uživatel následně navštíví jiný web, který je podvodný (může to být například web, který obsahuje škodlivý skript nebo odkaz).
- Tento podvodný web pošle požadavek na server cílové aplikace, kde je uživatel stále přihlášený. Tento požadavek může například měnit uživatelské údaje, odeslat peníze nebo provádět jiné akce, které uživatel normálně provádí na dané stránce.
- Protože uživatel je přihlášený a jeho cookies jsou automaticky odeslány společně s požadavkem, server cílové aplikace nepozná, že požadavek odeslala zcela jiná webová stránka a považuje požadavek za legitimní. Následně tedy provede požadovanou akci. Útočník pochopitelně odesílá požadavky na server prostřednictvím skriptu v pozadí. Uživatel tedy vůbec neví, že jeho akce (například odeslání formuláře nebo změna nastavení) byla provedena.
Teď nás asi napadá, proč by měl uživatel z důvěryhodné stránky (banka, e-shop) odcházet na nějakou falešnou stránku, že? Ale ono to mohlo být naopak. Uživatel na té škodlivé stránce již mohl být, pak otevřít tu důvěryhodnou. Například si otevřete několik e-shopů a pak se v jednom z nich rozhodnete nakoupit. A co když jeden z právě otevřených e-shopů není jen obyčejný obchod? Že to není možné? Tak se někdy podívejte, kolik máte v prohlížeči otevřených oken, až budete nakupovat v nějakém e-shopu, nebo pak platit v bance!
- Pozn:
- Abychom výše uvedené uvedli na pravou míru, musíme říci, že se často mylně předpokládá, že pouze CORS slouží jako ochrana proti CSRF útokům! Ve skutečnosti CORS pouze kontroluje, zda lze provést cross-origin požadavky, tedy požadavky mezi různými doménami. CSRF útoky se ale mohou odehrávat i v rámci stejné domény, kde CORS vůbec nezasahuje. Samotné CORS tedy nechrání přímo proti CSRF – ochranu je nutné implementovat na serverové straně, ale CORS je nezbytnou součástí komplexnějšího řešení (je nezbytné kombinovat CORS s dalšími bezpečnostními mechanismy).
Tak tedy CORS! Ale co teď s ním?
Pravidla CORS se objevila jako reakce na zvýšené požadavky na bezpečnost webových aplikací, zejména v kontextu rozmachu API (rozhraní pro programování aplikací) a častého komunikování mezi různými doménami. Představili je vývojáři webových prohlížečů a jejich přísné uplatňování začalo být vyžadováno zejména po roce 2010, kdy se staly běžnou součástí bezpečnostních standardů pro webové aplikace.
Jakmile tato pravidla začala být striktně vynucována, mnoho uživatelů a vývojářů (zejména v komunitě DIY) se dostalo do potíží, když jejich aplikace nebo projekty nedokázaly komunikovat s jinými servery či API, neboť nesplňovaly požadavky na správnou konfiguraci CORS. Pochopení a implementace CORS tak dnes patří mezi nezbytné kroky pro každý projekt, který pracuje s webovými technologiemi a vyžaduje síťovou komunikaci mezi různými doménami.
Jak nastavit pravidla pro hladkou komunikaci
Aby komunikace mezi klientem a serverem probíhala správně s ohledem na CORS, musí klient i server správně nastavit příslušné CORS hlavičky.
Podívejme se na příklad požadavku z klientské strany a odpovědi ze strany serveru.
Požadavek od klienta
Při odesílání požadavku s XMLHttpRequest (nebo Fetch API) bude klient zasílat požadavek s určitými hlavičkami. Pokud požadavek obsahuje některé „ne-simplified“ hlavičky (například Authorization
, Cache-Control
, nebo vlastní hlavičky), prohlížeč automaticky spustí tzv. preflight požadavek (OPTIONS), aby zjistil, zda server povoluje tento typ požadavku.
Požadavek (příklad s XMLHttpRequest):
Ukážeme si, jak prostřednictvím JavaScriptu odeslat dotaz na server s adresou http://nejaky-web.cz
(komunikující na portu port
) a dotazujeme se na soubor stranka.htm
.
Následující úryvek JavaScriptu ukazuje, jak se používá XMLHttpRequest (XHR), což je objekt pro práci s HTTP požadavky v JavaScriptu. XHR je základní technologií, na které staví AJAX (Asynchronous JavaScript and XML), což je způsob, jak aktualizovat části webové stránky bez nutnosti jejího kompletního načítání. Objekt XHR v naší ukázce umožní asynchronní komunikaci mezi klientem (např. webovou stránkou s tímto skriptem) a serverem:
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://nejaky-web.cz:port/stranka.htm", true);
// Přidání některých hlaviček (např. pro CORS)
xhr.setRequestHeader("Cache-Control", "no-cache"); // Uz i tato hlavička může způsobit problém, pokud není server nastaven na její přijetí
xhr.setRequestHeader("Authorization", "Bearer token"); // Příklad autorizace, která určitě spustí preflight
// Poslání požadavku
xhr.send();
xhr.onload = function() {
if (xhr.status === 200) {
console.log("Response:", xhr.responseText);
} else {
console.error("Error:", xhr.statusText);
}
};
xhr.onerror = function() {
console.error("Request failed");
};
První řádek vytváří nový objekt xhr, který je instancí třídy XMLHttpRequest
. Tento objekt bude použit pro odesílání a přijímání HTTP požadavků.
Metoda open()
se používá pro nastavení požadavku. První parametr „GET“ znamená, že chceme použít HTTP metodu GET, která slouží pro získání dat ze serveru. Druhý parametr je URL adresou, na kterou chceme poslat požadavek. Třetí parametr (zde nastavený na true) určuje, že požadavek bude asynchronní. To znamená, že skript nečeká na odpověď od serveru a může pokračovat v jiných operacích.
Metoda setRequestHeader()
umožňuje nastavit specifické HTTP hlavičky, které budou odeslány spolu s požadavkem. V tomto případě jsou přidány dvě hlavičky:
Cache-Control
: Tato hlavička určuje, jak by měl být obsah cachován (uložen do mezipaměti). Hodnota no-cache znamená, že by se neměl používat uložený obsah, ale vždy by měl být požadavek znovu odeslán na server.Authorization
: Tato hlavička se používá pro přenos autorizačních informací, zde je ukázán příklad použití tokenu typu Bearer pro autentizaci.
Chceme-li být moderní, můžeme použít modernější podobu skriptu. V dnešní době je XHR ve většině nových aplikací nahrazeno modernějšími alternativami, jako je Fetch API, které poskytuje jednodušší a flexibilnější způsob, jak provádět HTTP požadavky. Náš skript by pak vypadal následovně:
// Nastavení URL a hlaviček
const url = "http://nejaky-web.cz:port/stranka.htm";
const headers = {
"Cache-Control": "no-cache", // Uz i tato hlavička může způsobit problém, pokud není server nastaven na její přijetí
"Authorization": "Bearer token" // Příklad autorizace, která určitě spustí preflight
};
// Odeslání požadavku s Fetch API
fetch(url, {
method: "GET", // HTTP metoda (GET)
headers: headers // Hlavičky
})
.then(response => {
if (!response.ok) {
throw new Error('Síťová odpověď byla špatná');
}
return response.text(); // Nebo .json() pokud očekáváš JSON odpověď
})
.then(data => {
console.log('Úspěch:', data); // Zpracování odpovědi
})
.catch(error => {
console.error('Chyba:', error); // Zpracování chyby
});
Metoda fetch()
je moderní metoda pro odesílání HTTP požadavků. Prvním parametrem je URL adresou, na kterou požadavek posíláme – v tomto případě používáme metodu GET pro získání dat ze serveru (nastaveno parametrem method
).
Hlavičky se předávají jako objekt v parametru headers (nastaveno v proměnné headers
).
Metoda .then()
je součástí tzv. Promise v JavaScriptu a používá se pro práci s asynchronními operacemi, jako je například volání API nebo načítání dat. Promise je objekt, který reprezentuje hodnotu, která může být k dispozici nyní, nebo v budoucnu (např. po dokončení asynchronní operace). Metoda .then()
umožňuje definovat, co se má stát, když Promise úspěšně dokončí svou práci. Takže pokud máme asynchronní operaci (např. volání fetch()
), .then()
nám umožní reagovat na úspěšnou odpověď, která je vrácena.
Co se děje v tomto kódu?
První .then()
se spustí, když požadavek fetch()
vrátí odpověď. Odpověď je objekt Response, který obsahuje informace o odpovědi od serveru (např. status kód). Pokud odpověď není úspěšná (response.ok
je false
), vyhodí se chyba. Jinak se vrátí textová odpověď (response.text()
), která bude předána dalšímu .then()
. Druhý .then()
se spustí po úspěšném získání textové odpovědi. Data (v tomto případě textová odpověď) se předají do funkce, která je vypíše do konzole.
Metoda .catch()
se spustí pokud dojde k jakékoliv chybě během celého řetězce (např. síťová chyba nebo problém s odpovědí serveru) – zachytí chybu a provede kód uvnitř.
Tento způsob je mnohem modernější (a jsou lidé, kteří tvrdí, že i jednodušší), než je starší XMLHttpRequest.
Ať již použijeme kteroukoliv z metod odeslání požadavku, měl by být průběh síťové komunikace stejný.
Kdy a proč se provádí preflight požadavek?
Preflight požadavek je speciální dotaz typu OPTIONS
, který prohlížeč automaticky odešle na server ještě před samotným hlavním požadavkem. Jeho cílem je zjistit, zda server dovoluje požadovanou metodu a hlavičky v cross-origin komunikaci.
Preflight požadavek se spustí, pokud požadavek nesplňuje podmínky tzv. jednoduchého požadavku (simple request). Požadavek je jednoduchý, pokud splňuje všechny následující podmínky:
- Používá pouze metody
GET
,POST
neboHEAD
. - Nepoužívá vlastní hlavičky kromě těchto povolených:
Accept
Accept-Language
Content-Language
Content-Type
(s hodnotamiapplication/x-www-form-urlencoded
,multipart/form-data
nebotext/plain
)
- Nepoužívá žádnou autentizaci, tedy neposílá cookies (credentials: include) ani autentizační hlavičky (Authorization).
Pokud požadavek některou z těchto podmínek nesplňuje (například používá metodu PUT
, obsahuje hlavičku Authorization
nebo má Content-Type: application/json
), prohlížeč před ním automaticky odešle preflight dotaz OPTIONS
s informací o plánované metodě a hlavičkách.
Příklad preflight požadavku (odesílaného prohlížečem):
OPTIONS /stranka.htm HTTP/1.1
Host: nejaky-web.cz
Origin: http://nejaky-web.cz
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Cache-Control, Authorization, Content-Type
Zde prohlížeč informuje server, že klient chce poslat požadavek metodou PUT
s hlavičkami Cache-Control
, Authorization
a Content-Type
. Server na to musí správně odpovědět.
Odpověď serveru
Server musí hlavičky načít a správně vyhodnotit. Ve své odpovědi na preflight požadavek je třeba nastavit správné hlavičky CORS, aby prohlížeč věděl, že požadavek je povolen. Server by měl odpovědět hlavičkami, které umožní požadavek z místa jiného původu.
Odpověď na preflight požadavek (OPTIONS):
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://nejaky-web.cz
Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS
Access-Control-Allow-Headers: Cache-Control, Authorization, Content-Type
Access-Control-Max-Age: 86400
Tato odpověď serveru je příkladem hlaviček, které se používají k určení, jaké požadavky mohou být zpracovány serverem, pokud přicházejí z jiných domén (než je ta, na které běží server). Tato odpověď říká, že server umožňuje požadovanou metodu PUT i uvedené hlavičky a že tato pravidla lze uchovat v cache (Max-Age), aby se preflight požadavek nemusel znovu posílat při každém dotazu.
Pokud server neodpoví správně (například nevrátí Access-Control-Allow-Methods: PUT
), hlavní požadavek bude blokován a uživatel uvidí chybu CORS.
Hlavička Access-Control-Allow-Origin
určuje, které domény mají povolený přístup k serverovým zdrojům při cross-origin požadavcích. Lze ji nastavit několika způsoby:
Omezení na konkrétní doménu: Doporučená varianta pro bezpečné API – například:
Access-Control-Allow-Origin: https://moje-aplikace.cz
Tím se přístup povolí pouze požadavkům z https://moje-aplikace.cz
.
Povolení všech domén (*
tzv. „wildcard“):
Access-Control-Allow-Origin: *
Použití znaku *
znamená, že server povoluje přístup z jakékoli domény, což může být bezpečnostní riziko,
zejména pokud API pracuje s citlivými daty nebo vyžaduje autentizaci.
*
(wildcard) v této hlavičce umožňuje přístup ze všech domén, což může být problematické, pokud se v požadavku přenášejí cookies nebo jiné autentizační údaje. Pokud je potřeba povolit přístup pouze konkrétní aplikaci, měl by býtAccess-Control-Allow-Origin
nastaven na konkrétní doménu (např.http://muj-web.cz
).- Pokud server vyžaduje autentizaci pomocí cookies nebo HTTP autentizace, nastavení
Access-Control-Allow-Origin: *
nefunguje. Prohlížeč automaticky zablokuje požadavek, pokud je odeslán s credentials: include (tj. když klient požaduje odeslání cookies nebo autentizačních hlaviček).
Pokud API potřebuje povolit autentizované požadavky, musí hlavičky vypadat takto:
Access-Control-Allow-Origin: http://muj-web.cz
Access-Control-Allow-Credentials: true
Tím se umožní přístup pouze z konkrétní domény a zároveň server povolí prohlížeči odeslat cookies nebo autentizační hlavičky.
- Pozn:
- Pokud API vyžaduje autentizaci, NELZE použít
Access-Control-Allow-Origin: *
. HlavičkuAccess-Control-Allow-Origin: *
nelze použít současně sAccess-Control-Allow-Credentials: true
– to je častá chyba!
Odpověď na samotný požadavek (GET):
Jakmile preflight projde, a server na dotaz OPTION
klientovi odpoví, prohlížeč (klient) zašle skutečný požadavek GET, který server musí zpracovat.
GET /stranka.htm HTTP/1.1
Host: nejaky-web.cz
Origin: http://nejaky-web.cz
Cache-Control: no-cache
Odpověď na tento požadavek bude vypadat již jako klasická odpověď serveru, například takto:
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: http://nejaky-web.cz
Ale opět musí být doplněn zopakovanými hlavičkami pro CORS:
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Headers: Cache-Control
Access-Control-Max-Age: 86400
Zpracování odpovědi na straně klienta:
Pokud server správně odpoví s nastavenými CORS hlavičkami, prohlížeč umožní klientovi přístup k odpovědi, a to i když pochází z jiného původu (origin).
Shrnutí komunikace klient-server s CORS:
Bylo toho nějak moc? O.K., zkusíme to shrnout. Ještě jednou se podíváme, co dělá webový prohlížeč (klient) a co odesílá server:
- Klient (prohlížeč)
- Rád by odeslal požadavek s hlavičkami, které mohou vyžadovat preflight (např.
Cache-Control
,Authorization
apod.). - Prohlížeč pošle preflight požadavek OPTIONS, V hlavičce
Origin
odesílá adresu webu, ze kterého se dotazuje, také zasílá seznam metod a hlaviček, pro která žádá svolení (Access-Control-Request-Method
,Access-Control-Request-Headers
)
- Rád by odeslal požadavek s hlavičkami, které mohou vyžadovat preflight (např.
- Server
- Přijme preflight požadavek
OPTIONS
s výčtem hlaviček, metod a adresou webu jiného původu (Origin) - Po kontrole server odpovídá, ze kterého webu akceptuje dotaz (
Access-Control-Allow-Origin
), jaké jsou povolené metody (Access-Control-Allow-Methods
) a jaké jsou povolené hlavičky (Access-Control-Allow-Headers
). - Je-li dotaz z webu nepovoleného webu (nesouhlasí
Origin
s povolenou doménou) server dotaz odmítá a ukončuje.
- Přijme preflight požadavek
- Klient
- Po úspěšné odpovědi na preflight zasílá dotaz schválenou metodou (např. GET) s povolenými hlavičkami.
- Server
- Zpracovává a odesílá odpověď na požadavek (např. GET) s CORS hlavičkami v odpovědi.
- Klient
- Přijímá odpověď od serveru, kontroluje CORS náležitosti a pokud je vše O.K. zobrazí výsledek. Jinak přijatou odpověď blokuje a data zahazuje.
Pokud server správně nastaví CORS hlavičky a klient pošle požadavek správně, komunikace bude fungovat bez problémů.
Ukázka realizace na modulu Arduino
Teorie bylo dost, nyní se (konečně) zaměříme na praktické řešení ukázkového DIY projektu. Z předešlých odstavců pro nás plynou dvě možnosti pojetí našeho DIY projektu:
- Vyhnout se vyvoláním pravidel CORS tím, že budeme používat jen tzv. jednoduché požadavky (simple request).
- Připravit naši aplikaci na komunikaci dodržující základní pravidla CORS komunikace.
Ukážeme tu variantu druhou, tedy s nastavením komunikace mezi webovou stránkou a hardwarem respektující pravidla CORS. Konkrétně si představíme situaci, kdy na naší řídicí webové stránce máme javascriptový skript, který bude vzdáleně řídit modul Arduino UNO s připojeným Ethernet Shield. Tento scénář je běžný v rámci DIY projektů, kde je potřeba odesílat řídicí síťové požadavky mezi webovým frontendem a fyzickým zařízením. Předpokládáme, že řídicí webová stránka se nenačítá přímo ze serveru modulu Arduino, ale je na nějakém jiném serveru (například na webových stránkách hostovaných bůhvíkde). Abychom tedy v tomto případě zajistili, že webová stránka bude moci bez problémů komunikovat s Arduinem, musíme správně nastavit pravidla CORS, neboť jde o situaci tzv. cross-domain komunikace.
Nebudeme se tolik zaměřovat na zaslání požadavku klinetem, konec konců to zpravidla zvládne sám prohlížeč, ve kterém budeme mít spuštěnou řídicí webovou stránku, ale především se podíváme, jak tuto pravidla zpracovat na straně serveru v modulu Arduino. Právě neošetřené CORS na straně síťového serveru DIY projektu, jak jsme si již naznačili, bývalo při práci s různými doménami častým problémem a příčinou nezdaru.
Webová stránka (strana klienta):
Vzhledem k tomu, že komunikujete mezi webovou stránkou (JavaScript běžící v prohlížeči) a Arduino serverem, který je
pravděpodobně nasazen na jiném zařízení, musíme se vypořádat s problematikou CORS. Pokud má náš web (například http://muj-web.cz
) komunikovat s Arduino serverem běžícím na jiné IP adrese (například http://192.168.1.177
), prohlížeč bez dodržení CORS to standardně zablokuje. A to nechceme!
Následující HTML kód bude vytvářet webovou stránku s dvěma tlačítky pro rozsvícení a zhasnutí vestavěné LED na modulu
Arduino. Pro síťovou komunikaci použijeme již moderní javascriptovou metodu fetch. Budeme odesílat dva síťové požadavky na http://192.168.1.177
, první požadavek bude on
pro rozsvícení, druhý off
pro zhasnutí LED.
Webová stránka odesílající požadavky:
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Arduino LED Ovládání</title>
</head>
<body>
<div class="container">
<h1>Ovládání LED na Arduinu</h1>
<div class="button-container">
<button id="led-on">Zapnout LED</button>
<button id="led-off">Vypnout LED</button>
</div>
<div id="status" class="status">Stav: Nezjištěno</div>
</div>
<script>
// Funkce pro zapnutí LED
document.getElementById('led-on').addEventListener('click', function() {
fetch("http://192.168.1.177/on", {
method: "GET",
headers: {
"Cache-Control": "no-cache"
}
})
.then(response => response.text())
.then(data => {
console.log(data);
document.getElementById('status').innerText = "Stav: LED je zapnutá";
})
.catch(error => {
console.error('Chyba:', error);
document.getElementById('status').innerText = "Chyba při zapnutí LED";
});
});
// Funkce pro vypnutí LED
document.getElementById('led-off').addEventListener('click', function() {
fetch("http://192.168.1.177/off", {
method: "GET",
headers: {
"Cache-Control": "no-cache"
}
})
.then(response => response.text())
.then(data => {
console.log(data);
document.getElementById('status').innerText = "Stav: LED je vypnutá";
})
.catch(error => {
console.error('Chyba:', error);
document.getElementById('status').innerText = "Chyba při vypnutí LED";
});
});
</script>
</body>
</html>
Pokud chceme naši ovládací stránku trochu „vyfešákovat“, přidáme ještě do sekce <head>…</head> definici CSS stylů použitých elementů webové stránky:
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
text-align: center;
background-color: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 24px;
margin-bottom: 20px;
}
.button-container button {
background-color: #4CAF50;
color: white;
border: none;
padding: 10px 20px;
font-size: 18px;
margin: 10px;
border-radius: 5px;
cursor: pointer;
}
.button-container button:hover {
background-color: #45a049;
}
#status {
margin-top: 20px;
font-size: 18px;
font-weight: bold;
}
.status {
color: #333;
}
</style>
Získáme pak následující řídicí webovou stránku:
Modul Arduino:
Předpokládejme, že používáte Arduino UNO s Ethernet Shieldem a programujete ho v prostředí Arduino IDE (programovací jazyk Wire). Pro ovládání síťového rozšíření Ethernet Shield využíváme knihovnu Ethernet
. Kromě klasického kódu pro server musíte explicitně povolit CORS tím, že budeme korektně zpracovávat dotazy podle předešlých pravidel. Jak uvidíme, tak to zase nebude těžké. Je třeba jen vyřešit dva následující úkoly: podchycení dotazu OPTION a přidání odpovídajících CORS hlaviček na straně serveru Arduino.
Kód pro server na modulu Arduino:
#include <SPI.h>
#include <Ethernet.h>
// Nastavení MAC adresy a IP adresy (přizpůsobte dle potřeby)
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
IPAddress ip(192, 168, 1, 177); // Nastavte statickou IP dle potřeby
EthernetServer server(80); // HTTP server na portu 80
void setup() {
// Start Ethernet
Ethernet.begin(mac, ip);
server.begin();
// Nastavení vestavěné LED jako výstup
pinMode(LED_BUILTIN, OUTPUT);
Serial.begin(9600);
Serial.println("Server is ready!");
}
void loop() {
EthernetClient client = server.available(); // Čekání na klienta
if (client) {
String request = "";
// Čtení požadavku od klienta
while (client.available()) {
char c = client.read();
request += c;
}
Serial.println(request); // Vypíše celý požadavek na sériový port
// Preflight požadavek (OPTIONS)
if (request.indexOf("OPTIONS") != -1) {
client.println("HTTP/1.1 200 OK");
sendCorsHeaders(client); // Odpověď na preflight požadavek
client.println(); // Prázdný řádek mezi hlavičkami a tělem odpovědi
}
// GET požadavek na /on (rozsvícení LED)
else if (request.indexOf("GET /on") != -1) {
digitalWrite(LED_BUILTIN, HIGH); // Rozsvítí LED
client.println("HTTP/1.1 200 OK");
sendCorsHeaders(client); // CORS hlavičky
client.println("Content-Type: text/plain");
client.println();
client.println("LED is ON");
}
// GET požadavek na /off (zhasnutí LED)
else if (request.indexOf("GET /off") != -1) {
digitalWrite(LED_BUILTIN, LOW); // Zhasne LED
client.println("HTTP/1.1 200 OK");
sendCorsHeaders(client); // CORS hlavičky
client.println("Content-Type: text/plain");
client.println();
client.println("LED is OFF");
}
// Pokud požadavek není platný, vrátíme 404
else {
client.println("HTTP/1.1 404 Not Found");
sendCorsHeaders(client); // CORS hlavičky
client.println("Content-Type: text/plain");
client.println();
client.println("404 Not Found");
}
// Zavření klienta
client.stop();
}
}
// Funkce pro odeslání správných CORS hlaviček
void sendCorsHeaders(EthernetClient &client) {
client.println("Access-Control-Allow-Origin: *"); // Povolení jakékoliv domény (nebo specifikujte konkrétní)
client.println("Access-Control-Allow-Methods: GET, OPTIONS"); // Povolené metody
client.println("Access-Control-Allow-Headers: Content-Type, Cache-Control"); // Povolené hlavičky
client.println("Access-Control-Max-Age: 86400"); // Jak dlouho může prohlížeč použít tento preflight výsledek
}
Tento kód pro Arduino vytváří jednoduchý HTTP server, který běží na Ethernetu a ovládá vestavěnou LED pomocí HTTP požadavků. Výchozí kód najdeme v řadě ukázkových programů pro tvorbu řídicích serverů na modulu Arduino. Zde jsme jen přidali pravidla CORS. V ukážeme si jeho činnost především se zaměřením naši CORS úpravu.
Po výchozím nastavení síťové komunikace je pro nás klíčový příkaz EthernetClient client = server.available()
, který v hlavní nekonečné smyčce čeká na dotaz příchozího klienta. Pokud je klient dostupný (if (client)
), začne se číst jeho požadavek (HTTP request). Po přečtení požadavku se provádí kontrola, zda požadavek obsahuje potřebné HTTP metody:
OPTIONS
: To je preflight požadavek pro CORS, který musí server správně zpracovat, aby umožnil přístup z jiných domén.GET /on
: Pokud požadavek obsahuje "GET /on", Arduino rozsvítí vestavěnou LED.GET /off
: Pokud požadavek obsahuje "GET /off", LED se vypne.- Pokud požadavek není žádný z očekávaných, vrátí server chybu
404 Not Found
.
Každá odpověď obsahuje HTTP hlavičky (např. "HTTP/1.1 200 OK") a CORS hlavičky, které umožňují přístup z různých domén. Tyto hlavičky jsou přidávány funkcí sendCorsHeaders (na konci kódu)
void sendCorsHeaders(EthernetClient &client) {
client.println("Access-Control-Allow-Origin: *");
client.println("Access-Control-Allow-Methods: GET, OPTIONS");
client.println("Access-Control-Allow-Headers: Content-Type, Cache-Control");
client.println("Access-Control-Max-Age: 86400");
}
Funkce sendCorsHeaders()
se zaměřuje na odesílání CORS hlaviček ve formátu HTTP odpovědi. Hlavička Access-Control-Allow-Origin
říká, jak již bylo zmíněno, které domény mohou přistupovat k prostředkům serveru. Znak *
znamená, že všechny domény mají povolený přístup (neomezený přístup). Což pro náš DIY projekt je asi dobré – zejména ve fázi testování. Pokud bychom chtěli omezit přístup jen na konkrétní doménu (např. https://muj-web.cz), změnili bychom tuto hodnotu na tu konkrétní adresu. Ostatní hlavičky případnému dotazu říkají, že budeme respektovat dotazy typy GET a OPTION, prohlížeč může vyžadovat nastavení kešování nebo určovat formát výstupních návratových hodnot (tady to spíše máme z důvodu, co kdyby to tam webový prohlížeč z nějakého důvodu do dotazu přidal – např. díky použití nějaké javascriptové knihovny, která si to tam přidá). Rovněž je asi rozumné nastavit delší dobu trvání pro zasílání preflight dotazů.
A máme hotovo!
Nyní by měl být problém s CORS vyřešen. Po kliknutí na tlačítka na řídící webové stránce odesílá prohlížeč dotazy, na které server odpovídá v rámci pravidel CORS. Jak vidíme, tak se jedná poměrně o jednoduchou úpravu, za kterou je však poměrně složitá teorie o síťové komunikaci.
Trocha pesimismu na závěr:
Dříve než se nadšeně pustíme do zkoušení, musíme se připravit na jedno velké zklamání, které ale neplyne z problematiky CORS! Výše uvedený scénář má jeden problém a to, že většina domácích routerů NAT (Network Address Translation), firewally a Wi-Fi bezpečnostní opatření mohou blokovat přímý přístup z veřejné webové aplikace (hostované na vzdáleném serveru) k zařízení v domácí síti. Arduino v domácí síti je připojeno k lokální síti, která není obvykle přístupná z veřejného internetu bez specifického nastavení (tzv. port forwarding nebo jiných technických řešení).
Pokud chceme, aby naše zařízení bylo přístupné z veřejného internetu, musíme:
- Konfigurovat port forwarding na routeru, aby požadavky z veřejné sítě byly směrovány na IP adresu modulu Arduino v domácí síti (v našem případě 192.168.1.177). K modulu Arduino bychom pak nepřistupovali přes výše uvedenou IP adresu (192.168.1.177), ale přistupovali bychom na základě dotazu na naši domácí síť a nějaký přidělený port (např. 12.34.56.78:8080), který by byl na routeru „nasměrován“ do naší sítě na IP adresu modulu Arduino (192.168.1.177).
- Mít veřejnou (pevnou) IP adresu, abychom se mohli v domácí síti s připojeným modulem Arduino připojit. Jakmile nám internetový provider tuto možnost neumožní, nebo pokud přiděluje IP adresu dynamicky, nebude dotazování do naší sítě možné. Pokud poskytovatel internetu používá CG-NAT (Carrier-Grade NAT), nemusí být port forwarding vůbec možný bez veřejné IP adresy nebo VPN.
Toto jsou však nastavení, která musí nejen náš hardware umožňovat (router), ale především jej musí nastavit správce dané sítě (port forwarding) a někdy i samotný poskytovatele připojení (veřejná pevná IP adresa).
Hlavička: Access-Control-Request-Private-Network
Na úplný závěr se ještě podíváme na jednu hlavičku. Hlavička Access-Control-Request-Private-Network
je součástí nové (a zatím stále experimentální) specifikace, která byla zavedena kvůli zlepšení bezpečnosti při přístupu k prostředkům na privátních sítích (například za firewallem nebo v lokální síti).
Tato hlavička se aktuálně testuje v některých prohlížečích (Google Chrome), některé prohlížeče ji ještě nepodporují. Server
ji musí explicitně povolit pomocí Access-Control-Allow-Private-Network: true
, jinak může být požadavek odmítnut. Pokud naše Arduino poběží v lokální síti a přistupujeme k němu z jiné sítě, nemusí jít jen o problém CORS, ale i o omezení prohlížeče (Chrome například v budoucnu plánuje blokovat přístup k lokálním IP bez povolení této hlavičky).
Pokud prohlížeč detekuje, že se jedná o požadavek na přístup k prostředkům na privátní síti, přidá do preflight požadavku (v rámci požadavku typu OPTIONS) hlavičku:
Access-Control-Request-Private-Network: true
Pokud server správně reaguje a povolí tento typ požadavku, může na něj odpovědět s příslušnými CORS hlavičkami, které umožní provedení hlavního požadavku. V opačném případě požadavek bude blokován. Server musí specificky reagovat na tuto hlavičku v preflight odpovědi a musí ji explicitně povolit, aby prohlížeč mohl pokračovat s hlavním požadavkem. Odpovědní hlavičky CORS by měly obsahovat Access-Control-Allow-Private-Network: true
, což naznačuje, že server tento typ požadavků povoluje.
Příklad odpovědi serveru na takový požadavek:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Private-Network: true
Tato hlavička informuje prohlížeč, že server povoluje přístup k prostředkům na privátní síti.
Protože nevíme, za jakých podmínek budeme náš DIY provozovat, mohlo by se tedy stát, že se nám povede vyvolat i tento požadavek. Měli bychom jej tedy také nějak ošetřit. Upravíme v programu pro Arduino naši sérii podmínek pro zpracování požadavku a přidáme ještě jednu podmínku:
// Preflight požadavek (OPTIONS)
if (request.indexOf("OPTIONS") != -1) {
client.println("HTTP/1.1 200 OK");
sendCorsHeaders(client); // Odpověď na preflight požadavek
if (request.indexOf("Access-Control-Request-Private-Network: true") != -1) {
client.println("Control-Allow-Private-Network: true"); // povolení privátních sítí
}
client.println(); // Prázdný řádek mezi hlavičkami a tělem odpovědi
}
Jakmile server zaznamená v dotazu výskyt dotazu na privátní síť, odpoví přidáním hlavičky Control-Allow-Private-Network: true
do odpovědi. Teoreticky by mělo stačit do odpovědi na OPTION, kdyby s tím nastával problém, bylo by třeba ošetřit přidání dané hlavičky pro všechny odpovědi přímo ve funkci sendCorsHeaders()
.
Závěr:
CORS je klíčovým bezpečnostním mechanismem, který chrání webové aplikace před neoprávněným přístupem a útoky. Pro vývojáře DIY projektů může být jeho správná konfigurace výzvou, ale pochopení principů, jako jsou preflight požadavky a správné nastavení serverových hlaviček, umožňuje bezproblémovou komunikaci mezi různými doménami. V článku jsme si ukázali praktický příklad, jak tyto zásady aplikovat při komunikaci mezi webovou aplikací a Arduinem, což je častý scénář v IoT a domácí automatizaci.
Správná implementace CORS však není jediným aspektem, který je nutné při síťové komunikaci zohlednit. Faktory, jako je NAT, firewall nebo dynamické IP adresy, mohou ovlivnit dostupnost zařízení z veřejného internetu. Proto je důležité nejen porozumět pravidlům CORS, ale také zvážit celkovou síťovou architekturu a bezpečnostní opatření. Se správným přístupem lze i v rámci DIY projektů vytvořit stabilní a bezpečné síťové řešení, které umožní efektivní vzdálenou komunikaci.