Kde se asi stala chyba: Proxování WebSocketů

Tento příspěvek se bude Pythonu dotýkat pouze okrajově, nicméně i tak server psaný v Pythonu v něm bude hrát velkou, ne-li hlavní roli. Příspěvek se bude zabývat tím, jak pomocí webového serveru Apache vytvořit proxy k WebSocket serveru postaveného nad Python frameworkem Tornado.

Představme si nejprve hlavní herce dnešního představení. V první řadě je WebSocket server postavený na Python frameworku Tornado. Tornado je výborné tam, kde je třeba řešit stovky zároveň otevřených spojení, které jsou obsluhovány pomocí non-blocking IO v rámci jednoho (ale volitelně i více) procesu. Pro naše užití jsme Tornado vybrali ale kvůli jiné vlastnosti, kterou je možnost jednoduše vytvořit WebSocket server v Pythonu.

Vytvoření WS serveru odpovídá naimplementování vlastního handleru, potomka třídy WebSocketHandler. Z tohoto potomka je možné již obsluhovat základní WS primitiva — open, on_close, on_message a write_message. Námi naimplementovaný WebSocketHandler následně „zavěsíme“ do Tornado HTTP aplikace například následujícím způsobem:

application = tornado.web.Application([
    (r'/tts/mp3/(.*)', ResultHandler),
    (r'/tts/wav/(.*)', ResultHandler),
    (r'/tts', WSHandler),
])

Pro naše účely stačí říci, že ResultHandler je potomek StaticFileHandler a slouží k servování statických souborů z pevného disku, WSHandler je pak WebSocket handler.

Vytvořená aplikace funguje výborně, bez problémů se nechá napojit na HTML front-end a nic nenasvědčuje budoucím problémům.

O pár měsíců později…

Jak už to tak bývá, o pár měsíců později vyvstala potřeba použít SSL, respektive HTTPS na takto provozovaném serveru. Protože jsme HTTPS chtěli provozovat pro všechny služby na dané doméně pouze na jediném portu, rozhodli jsme se proxovat vše přes Apache, který bude poslouchat na portu 443, část provozu obslouží napřímo a část předá Tornado serveru na localhostu a portu 8001.

Chvilka hledání ukázala, že Apache 2.4 z Debianu testing již obsahuje modul mod_proxy_wstunnel, který lze velice jednoduše použít pro proxování WebSocketů, prakticky ve stejném duchu, jako se to děje u běžného HTTP provozu. V konfiguraci Apache stačí uvést:

ProxyPass /tts-ws ws://127.0.0.1:8001/

Vše na první pohled funguje bez problémů, všechny požadavky na ws://server/tts-ws/tts jsou obsluhovány Tornado back-endem přesně jak je potřeba. Nicméně pokud se vytvořilo netriviální množství WebSocket spojení, Apache se začal chovat velice nepředvídavě. Dokonce se zdálo, že tato konfigurace proxy na sebe stahuje i okolní provoz a Tornado pak vrací chyby 404 i pro stránky, které normálně existují a po refreshi stránky, popřípadě otevření v jiném okně normálně fungují.

Kde se asi stala chyba?

V tomto případě hledání odpovědi na tuto otázku zabralo téměř dvě celá odpoledne. Řešení je nakonec triviální a pokud jej člověk objeví, tak i naprosto logické. Nicméně samo o sobě jej nenajdete v žádné dokumentaci ani fóru. Problém samotný je způsoben tím, že pomocí proxy z cesty /tts-ws je obsluhován i klasický HTTP provoz (cesty /tts-ws/tts/mp3 a /tts-ws/tts/wav). Díky návrhu WebSocketů jako rozšíření HTTP toto funguje, pokud však Apache začne znovupoužívat proxy workery a připojení se o ně začnou střídat, dějí se nepředvídatelné věci.

Řešení samotné spočívá v explicitním proxování HTTP provozu na cesty /tts-ws/tts/* pomocí konfigurace:

ProxyPass /tts-ws/tts/mp3 http://127.0.0.1:8001/tts-ws/tts/mp3 
ProxyPass /tts-ws/tts/wav http://127.0.0.1:8001/tts-ws/tts/wav 
ProxyPass /tts-ws ws://127.0.0.1:8001/

Konfigurace HTTP proxy podcest musí být uvedena před WS proxy nadřazené cesty.

V okamžiku, kdy byla příčina odhalena a opravena jsem se v duchu odměnil za výborný nápad navrhnout WebSocket server tak, že rodičovská cesta odkazuje na WebSocket handler a její děti jsou obsluhovány přes klasické HTTP.

Řešení je opět z kategorie „levou zadní“, nicméně cesta k němu je trnitá a dlouhá, plná experimentů a testování. A navíc — tato cesta není lemována debuggery ani ladícími výpisy, obdobně jako v prvním článku Kde se asi stala chyba.

Napsat komentář