Připíchněte si SSL certifikát k doméně

17. 9. 2014
Doba čtení: 10 minut

Sdílet

Ilustrační obrázek
Autor: Depositphotos – stori
Ilustrační obrázek
Bezpečnost je v posledních měsících velké téma a jedním z klíčových bodů je téma SSL, certifikátů, autorit a modelu PKI. Pokud ale šifrujeme, měli bychom to dělat správně a zabývat se i autentizací. Tady nám ale do toho vstupují stovky certifikačních autorit, kterým „věříme“. Co když jim ale nevěříme zase tolik?

Šifrujte, šifrujte, šifrujte, zní teď odevšad parafrázovaná slova klasika. Nástroje na to máme, spousta věcí se dá automatizovat, takže hurá do toho. Šifrování je ovšem jedna věc, ale stejně zásadní je i autentizace. K čemu je nám šifrování, když na druhé straně sedí útočník, který se nám vesele vydává za původní protistranu? Musíme proto zajistit ověření obou stran komunikace, abychom svá tajemství nevyzrazovali leckomu.

Nejčastěji se při tom skloňuje SSL (věděli jste, že i SSH umí certifikáty?), což je logické, protože většina služeb je dnes dostupných po webu. Díky SSL, infrastruktuře veřejného klíče (PKI) a hierarchii certifikačních autorit máme v ruce mocný nástroj, který dokáže vše zvládnout naprosto automaticky. Za správce i za uživatele. Bohužel i tady platí, že je to dobrý sluha, ale velmi zlý pán.

Teoretický úvod

SSL certifikát není vlastně nic jiného, než soubor v konkrétním formátu, který obsahuje několik různých informací: datum platnosti, informace o držiteli, informace o autoritě, veřejný klíč držitele a to celé je podepsané onou autoritou. Certifikát tak vlastně říká: „Já, autorita, potvrzuji, že jsem ověřil existenci subjektu, kterému je tento certifikát vystaven. Držitel privátního klíče může tento certifikát použít k následujícím účelům v této vyhrazené době.“

Je to vlastně úplně stejné jako třeba u pasu. Také tam existuje autorita (stát), která ověřuje identitu osob, a pokud vše souhlasí, vystaví jim pas. V něm je uvedeno, kdo pas vystavil, kdo je držitelem, jak vypadá, kdy je pas platný a jaká má například omezení.

Takový certifikát si samozřejmě může vystavit úplně kdokoliv. Můžete si vytvořit vlastní certifikační autoritu pro celou firmu a tou pak podepisovat vydávané certifikáty pro své zaměstnance. Protože považujete za důvěryhodnou svou autoritu, můžete věřit i tomu, kdo se prokáže jí vydaným certifikátem. Druhou variantou jsou takzvané self-signed certifikáty, které jsou podepsané samy sebou. Ty nemají žádnou autoritu a existují mimo hierarchii jen jako jeden objekt, kterému buď věříte nebo ne.

Jako děti jsme si taky vyráběly své pasy. Stačí k tomu papír, nůžky, sešívačka a pastelky. Namalujete fotku, vyplníte údaje a máte pas své vlastní republiky. Rozdíl mezi tímto a „pravým“ pasem je v tom, že vaše „autorita“ není příliš důvěryhodná a za pytlík bonbónů jste ochotni namalovat takový pas komukoliv. Proto váš pas ostatní státy neakceptují jako platný doklad.

Aby tohle fungovalo, musí existovat seznam autorit, kterým apriori věříme. Jimi vydané certifikáty považujeme za důvěryhodné a přeneseně tak věříme i tomu, kdo se takovým certifikátem prokáže. Seznam důvěryhodných autorit (i s veřejnými klíči) k nám přichází obvykle s operačním systémem a jednotlivými aplikacemi. Problém je, že je za nás vybírá někdo jiný a že je jich moc. Mozilla v tuto chvíli věří 359 autoritám po celém světě, které navíc často delegují svá práva na další a další organizace. Nakonec tak jde o tisíce různých autorit, které mohou vystavovat certifikáty k libovolným webům a službám. Věříme jim všem?

Opět zůstaneme u pasů. Pokud má celník v ruce podrobný manuál k orientaci v autoritách vydávajících pasy, má na něm 196 nezávislých států (číslo se občas liší, ale na tom teď nesejde), které považuje za autoritu oprávněnou vydávat pasy. Kterýkoliv z těchto států může vydat jakýkoliv pas a celník mu (alespoň teoreticky) věří. Může se ovšem stát, že nedopatřením, nátlakem nebo prostě z potřeb daného státu vznikne pas, který je sice „pravý“, ale obsahuje nesprávná data. James Bond by o tom mohl vyprávět. A ono se to opravdu děje.

Celý problém certifikačních autorit je tedy v tom, že my (respektive náš software) jim vlastně bezmezně věříme. Kterákoliv z tisíců autorit může vydat certifikát k jakémukoliv serveru a nám se bude zdát důvěryhodný. Neexistuje žádná vazba doména–autorita. Jakmile je jednou autorita v naší databázi důvěryhodných, může vydávat cokoliv pro kohokoliv. Ono se to běžně neděje, ale může se stát, že nedopatřením, nátlakem nebo prostě z potřeb daného státu…

Naštěstí si tohoto problému všimli jiní a už poměrně dávno. Vzniklo tak několik řešení, která je možné nasadit, abychom zmírnili dopad podvrženého důvěryhodného certifikátu.

TLSA z projektu DANE

Z projektu DANE při IETF vzešel nový DNS záznam TLSA. Ten může obsahovat otisk certifikátu či veřejného klíče, který je pak pevně svázán s konkrétní doménou. Aplikace tak mohou snadno provést dodatečnou kontrolu, která jim umožní ověřit, zda se serverem zaslaný certifikát shoduje s otiskem v zóně. TLSA záznam musí být podepsán pomocí DNSSEC, aby byla informace o certifikátu přenesena bezpečně.

Více v článku: Protokol DANE aneb (z)krocení zlých certifikačních autorit

Implementace je velmi jednoduchá, jde vlastně jen o vygenerování jednoho dalšího záznamu do zóny. Jak vypadá, se můžete podívat jednoduše:

$ dig +short _443._tcp.www.linuxdays.cz tlsa
3 1 1 C03E8D287834E9B615DE0F8022C48BDAF4B4F3B440D37522C8B8993C E9B9031E

Detaily rozebírá RFC 6698, případně už zmíněný článek o DANE. První tři informace určují, o jaký otisk se jedná, zda jím určujete autoritu či konkrétní certifikát. V příkladu uvedená varianta 3 označuje otisk vlastního certifikátu, kterým se server prokazuje.

Implementujeme

Pokud chcete tento záznam pro svůj server vytvořit, budete potřebovat jediné: certifikát získaný ze svého serveru. Ten získáte buď kopií ze serveru, nebo na dálku pomocí švýcarského nožíku OpenSSL:

$ openssl s_client -showcerts -connect www.linuxdays.cz:443 /dev/null|openssl x509 -outform PEM > certifikat.pem

Teď už stačí použít utilitku Swede (napsaná v Pythonu) nebo webový TLSA generátor. Do něj stačí certifikát nakopírovat a vybrat správné parametry.

S utilitou Swede je to ještě jednodušší, umí si sama ověřit DNSSEC podpisy, stáhnout si certifikát a vytvořit potřebný TLSA záznam:

$ swede create --output rfc --usage 3 --selector 1 www.linuxdays.cz
No certificate specified on the commandline, attempting to retrieve it from the server www.linuxdays.cz.
Attempting to get certificate from 37.205.10.200
M2Crypto does not support SNI: services using virtual-hosting will show the wrong certificate!
Got a certificate with Subject: /description=Drwmx3MYVre6VTtN/C=SK/ST=Nitra/L=Sahy/O=\x00T\x00o\x00m\x00\xE1\x01a\x00 \x00S\x00r\x00n\x00a/CN=*.linuxdays.cz/[email protected]
_443._tcp.www.linuxdays.cz. IN TLSA 3 1 1 c03e8d287834e9b615de0f8022c48bdaf4b4f3b440d37522c8b8993ce9b9031e
Attempting to get certificate from 2a01:430:17:1::ffff:613
M2Crypto does not support SNI: services using virtual-hosting will show the wrong certificate!
Got a certificate with Subject: /description=Drwmx3MYVre6VTtN/C=SK/ST=Nitra/L=Sahy/O=\x00T\x00o\x00m\x00\xE1\x01a\x00 \x00S\x00r\x00n\x00a/CN=*.linuxdays.cz/[email protected]
_443._tcp.www.linuxdays.cz. IN TLSA 3 1 1 c03e8d287834e9b615de0f8022c48bdaf4b4f3b440d37522c8b8993ce9b9031e

Ověřujeme

K ověření je možné použít několik metod. Můžeme opět začít pomocí Swede:

$ swede verify www.linuxdays.cz
Received the following record for name _443._tcp.www.linuxdays.cz.:
    Usage:              3 (End-Entity)
    Selector:           1 (SubjectPublicKeyInfo)
    Matching Type:          1 (SHA-256)
    Certificate for Association:    c03e8d287834e9b615de0f8022c48bdaf4b4f3b440d37522c8b8993ce9b9031e
This record is valid (well-formed).
Attempting to verify the record with the TLS service...
Got the following IP: 37.205.10.200
M2Crypto does not support SNI: services using virtual-hosting will show the wrong certificate!
SUCCESS (Usage 3): The certificate offered by the server matches the TLSA record
The matched certificate has Subject: /description=Drwmx3MYVre6VTtN/C=SK/ST=Nitra/L=Sahy/O=\x00T\x00o\x00m\x00\xE1\x01a\x00 \x00S\x00r\x00n\x00a/CN=*.linuxdays.cz/[email protected]
Got the following IP: 2a01:430:17:1::ffff:613
M2Crypto does not support SNI: services using virtual-hosting will show the wrong certificate!
SUCCESS (Usage 3): The certificate offered by the server matches the TLSA record
The matched certificate has Subject: /description=Drwmx3MYVre6VTtN/C=SK/ST=Nitra/L=Sahy/O=\x00T\x00o\x00m\x00\xE1\x01a\x00 \x00S\x00r\x00n\x00a/CN=*.linuxdays.cz/[email protected]

Další možností je dostatečně nový GnuTLS:

$ gnutls-cli --dane www.linuxdays.cz
…
- Status: The certificate is trusted.
- DANE: Certificate matches.

Pro hračičky: Pokud si chcete na věci víc sáhnout, můžete TLSA záznam získat pomocí dig (viz výše) a vypočítat si klíč samostatně. Nejprve je z certifikátu (ten už máme) potřeba vyextrahovat veřejný klíč:

$ openssl x509 -noout -pubkey < certifikat.pem > klic.key

Pak stačí ze souboru s klíčem odstranit hlavičku a patičku a následujícím příkazem dekódovat z base64 a prohnat ho SHA funkcí:

$ base64 -d klic.key | sha256sum -b
c03e8d287834e9b615de0f8022c48bdaf4b4f3b440d37522c8b8993ce9b9031e

Z uživatelského hlediska je nejpříjemnější rozšíření DNSSEC/TLSA Validátor od CZ.NIC, které vše udělá za vás. Pokud navštívíte web s TLSA, ukáže vám v adresním řádku pěkné ikonky.

Pozor na to, že validátor nenahrazuje klasické PKI v prohlížeči, takže pokud použijete například self-signed certifikát nebo vlastní CA, prohlížeč vás i přes TLSA záznam upozorní na nedůvěryhodnost certifikátu. Vyzkoušet si to můžete na některé z testovacích stránek DANE Test Sites.

Přímou podporu TLSA má také mail server Postfix, takže si jej můžete nakonfigurovat podle návodu Ondřeje Surého. Mail server se pak bude podle TLSA řídit, a pokud bude záznam uveden, bude vyžadovat od serveru platný SSL certifikát.

CAA záznamy s instrukcemi pro certifikační autority

Úplně za jiný konec uchopil problémy současného PKI standard RFC 6844 z dílny známé certifikační autority Comodo. Řeší se tak jen jeden praktický problém současného PKI modelu, totiž že všechny autority jsou z hlediska klienta stejně důvěryhodné, ačkoli jsou v náročnosti autorizace značné rozdíly. Případný útočník tak může pro útok použít nejvíce benevolentní autoritu, kterou bude nepochybně snadnější uvést v omyl.

V současné době se takový problém částečně řeší jakýmsi neveřejným blacklistem, na kterém jsou zapsána obecná slova a doménová jména, která by autority bez důkladného ověření neměly vydávat. Problém je, že seznam je neveřejný a postup k zapsání na něj netransparentní.

RFC 6844 definuje nový typ DNS záznamu CAA, který má umožnit majitelům domény určit, které certifikační autority jsou oprávněné k vydání certifikátů pro danou doménu. To zní podobně jako TLSA záznam s polem usage=0, zásadní rozdíl ovšem je v tom, kdo takové záznamy validuje.

Pro účinnost TLSA záznamů je nutné je validovat na každém koncovém bodě komunikace, což s sebou nese požadavek na DNSSEC validaci. Něco takového je ideální stav, bohužel bude trvat hodně dlouho, než se něco takového stane i realitou. Záznam typu CAA je proti tomu určen pouze pro validaci certifikačními autoritami, a pouze jako poslední krok před vystavením certifikátu. K úspěšnému zavedení stačí tedy přinutit všechny certifikační autority, jejichž počet je mnohonásobně nižší v porovnání s počtem všech koncových bodů.

Dostane-li certifikační autorita pokyn k vydání certifikátu, zkontroluje přítomnost CAA záznamu v doméně. Pokud doména žádný záznam neobsahuje, má se za to, že CAA zde není podporováno a certifikát je vydán. Je-li však CAA záznam přítomen, je k vydání certifikátu potřebné, aby příslušný záznam obsahoval autorizaci pro danou autoritu. Je-li v certifikátu autorizace pro jinou autoritu, je vydání certifikátu odmítnuto a může o tom být automatizovaně podána zpráva ve formátu IODEF.

Jak vypadá CAA záznam

Pro praktickou ukázku se můžeme podívat například na doménu Comodo.com:

comodo.com.  IN TYPE257 \# 19 00056973737565636F6D6F646F63612E636F6D
comodo.com.  IN TYPE257 \# 35 0005696F6465666D61696C746F3A73736C6162
                              75736540636F6D6F646F63612E636F6D 

Po rozkódování generického typu 257 získáme takovouto mnohem srozumitelnější dvojici záznamů:

comodo.com.  IN CAA 0 issue "comodoca.com"
comodo.com.  IN CAA 0 iodef "mailto:[email protected]" 

Tedy certifikát pro comodo.com smí vystavit jen autorita comodoca.com a případné pokusy o vystavení jinou autoritou mají být hlášeny na uvedenou e-mailovou adresu.

bitcoin_smenarna

Validovati uživatelem zakázáno

Mohlo by se zdát, že informace z CAA záznamů by mohly posloužit nejen certifikačním autoritám, ale i koncovým systémům. Ve skutečnosti standard však právě takové použití zapovídá. Tím hlavním důvodem je, že certifikáty jsou obvykle vystavovány na dlouhou dobu, zatímco CAA záznamy ukazují pouze aktuálně platnou autoritu. Ta se může během času změnit, takže nesoulad mezi CAA záznamem a vydavatelem certifikátu není považován za chybu. Ostatně, pro validování autority koncovým systémem již existuje záznam typu TLSA a je tedy nežádoucí duplikovat jeho funkcionalitu.

Nápad dobrý, realizace pokulhává

Bohužel, přestože byl CAA záznam standardizován už v lednu 2013, stále se nedočkal velké podpory ze strany autorit. Na nich teď je, aby edukovaly své zákazníky o tom, že si mají do své domény CAA záznamy umístit a jaký má být jejich obsah. Z praktického hlediska se pak nejeví jako příliš vhodné umisťování dalších, potenciálně velmi dlouhých DNS záznamů do kořene (apexu) zóny, vzhledem k navýšení zesilujícího faktoru pro útoky zneužívající DNS.

Autor článku

Petr Krčmář pracuje jako šéfredaktor serveru Root.cz. Studoval počítače a média, takže je rozpolcen mezi dva obory. Snaží se dělat obojí, jak nejlépe umí.

Ondřej Caletka vystudoval obor Telekomunikační technika na ČVUT a dnes pracuje ve vzdělávacím oddělení RIPE NCC, mezinárodní asociaci koordinující internetové sítě.

'; document.getElementById('preroll-iframe').onload = function () { setupIframe(); } prerollContainer = document.getElementsByClassName('preroll-container-iframe')[0]; } function setupIframe() { prerollDocument = document.getElementById('preroll-iframe').contentWindow.document; let el = prerollDocument.createElement('style'); prerollDocument.head.appendChild(el); el.innerText = "#adContainer>div:nth-of-type(1),#adContainer>div:nth-of-type(1) > iframe { width: 99% !important;height: 99% !important;max-width: 100%;}#videoContent,body{ width:100vw;height:100vh}body{ font-family:'Helvetica Neue',Arial,sans-serif}#videoContent{ overflow:hidden;background:#000}#adMuteBtn{ width:35px;height:35px;border:0;background:0 0;display:none;position:absolute;fill:rgba(230,230,230,1);bottom:20px;right:25px}"; videoContent = prerollDocument.getElementById('contentElement'); videoContent.style.display = 'none'; videoContent.volume = 1; videoContent.muted = false; const playPromise = videoContent.play(); if (playPromise !== undefined) { playPromise.then(function () { console.log('PREROLL sound allowed'); // setUpIMA(true); videoContent.volume = 1; videoContent.muted = false; setUpIMA(); }).catch(function () { console.log('PREROLL sound forbidden'); videoContent.volume = 0; videoContent.muted = true; setUpIMA(); }); } } function setupDimensions() { prerollWidth = Math.min(iinfoPrerollPosition.offsetWidth, 480); prerollHeight = Math.min(iinfoPrerollPosition.offsetHeight, 320); } function setUpIMA() { google.ima.settings.setDisableCustomPlaybackForIOS10Plus(true); google.ima.settings.setLocale('cs'); google.ima.settings.setNumRedirects(10); // Create the ad display container. createAdDisplayContainer(); // Create ads loader. adsLoader = new google.ima.AdsLoader(adDisplayContainer); // Listen and respond to ads loaded and error events. adsLoader.addEventListener( google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, onAdsManagerLoaded, false); adsLoader.addEventListener( google.ima.AdErrorEvent.Type.AD_ERROR, onAdError, false); // An event listener to tell the SDK that our content video // is completed so the SDK can play any post-roll ads. const contentEndedListener = function () { adsLoader.contentComplete(); }; videoContent.onended = contentEndedListener; // Request video ads. const adsRequest = new google.ima.AdsRequest(); adsRequest.adTagUrl = iinfoVastUrls[iinfoVastUrlIndex]; console.log('Preroll advert: ' + iinfoVastUrls[iinfoVastUrlIndex]); videoContent.muted = false; videoContent.volume = 1; // Specify the linear and nonlinear slot sizes. This helps the SDK to // select the correct creative if multiple are returned. // adsRequest.linearAdSlotWidth = prerollWidth; // adsRequest.linearAdSlotHeight = prerollHeight; adsRequest.nonLinearAdSlotWidth = 0; adsRequest.nonLinearAdSlotHeight = 0; adsLoader.requestAds(adsRequest); } function createAdDisplayContainer() { // We assume the adContainer is the DOM id of the element that will house // the ads. prerollDocument.getElementById('videoContent').style.display = 'none'; adDisplayContainer = new google.ima.AdDisplayContainer( prerollDocument.getElementById('adContainer'), videoContent); } function unmutePrerollAdvert() { adVolume = !adVolume; if (adVolume) { adsManager.setVolume(0.3); prerollDocument.getElementById('adMuteBtn').innerHTML = ''; } else { adsManager.setVolume(0); prerollDocument.getElementById('adMuteBtn').innerHTML = ''; } } function onAdsManagerLoaded(adsManagerLoadedEvent) { // Get the ads manager. const adsRenderingSettings = new google.ima.AdsRenderingSettings(); adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true; adsRenderingSettings.loadVideoTimeout = 12000; // videoContent should be set to the content video element. adsManager = adsManagerLoadedEvent.getAdsManager(videoContent, adsRenderingSettings); // Add listeners to the required events. adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, onAdError); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, onContentPauseRequested); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, onContentResumeRequested); adsManager.addEventListener( google.ima.AdEvent.Type.ALL_ADS_COMPLETED, onAdEvent); // Listen to any additional events, if necessary. adsManager.addEventListener(google.ima.AdEvent.Type.LOADED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, onAdEvent); playAds(); } function playAds() { // Initialize the container. Must be done through a user action on mobile // devices. videoContent.load(); adDisplayContainer.initialize(); // setupDimensions(); try { // Initialize the ads manager. Ad rules playlist will start at this time. adsManager.init(1920, 1080, google.ima.ViewMode.NORMAL); // Call play to start showing the ad. Single video and overlay ads will // start at this time; the call will be ignored for ad rules. adsManager.start(); // window.addEventListener('resize', function (event) { // if (adsManager) { // setupDimensions(); // adsManager.resize(prerollWidth, prerollHeight, google.ima.ViewMode.NORMAL); // } // }); } catch (adError) { // An error may be thrown if there was a problem with the VAST response. // videoContent.play(); } } function onAdEvent(adEvent) { const ad = adEvent.getAd(); console.log('Preroll event: ' + adEvent.type); switch (adEvent.type) { case google.ima.AdEvent.Type.LOADED: if (!ad.isLinear()) { videoContent.play(); } prerollDocument.getElementById('adContainer').style.width = '100%'; prerollDocument.getElementById('adContainer').style.maxWidth = '640px'; prerollDocument.getElementById('adContainer').style.height = '360px'; break; case google.ima.AdEvent.Type.STARTED: window.addEventListener('scroll', onActiveView); if (ad.isLinear()) { intervalTimer = setInterval( function () { // Example: const remainingTime = adsManager.getRemainingTime(); // adsManager.pause(); }, 300); // every 300ms } prerollDocument.getElementById('adMuteBtn').style.display = 'block'; break; case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: if (ad.isLinear()) { clearInterval(intervalTimer); } if (prerollLastError === 303) { playYtVideo(); } break; case google.ima.AdEvent.Type.COMPLETE: if (ad.isLinear()) { clearInterval(intervalTimer); } playYtVideo(); break; } } function onAdError(adErrorEvent) { console.log(adErrorEvent.getError()); prerollLastError = adErrorEvent.getError().getErrorCode(); if (!loadNext()) { playYtVideo(); } } function loadNext() { iinfoVastUrlIndex++; if (iinfoVastUrlIndex < iinfoVastUrls.length) { iinfoPrerollPosition.remove(); playPrerollAd(); } else { return false; } adVolume = 1; return true; } function onContentPauseRequested() { videoContent.pause(); } function onContentResumeRequested() { videoContent.play(); } function onActiveView() { if (prerollContainer) { const containerOffset = prerollContainer.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight/1 && containerOffset.bottom > 0.0) { if (prerollPaused) { adsManager.resume(); prerollPaused = false; } return true; } else { if (!prerollPaused) { adsManager.pause(); prerollPaused = true; } } } return false; } function playYtVideo() { iinfoPrerollPosition.remove(); youtubeIframe.style.display = 'block'; youtubeIframe.src += '&autoplay=1&mute=1'; } }
'; document.getElementById('outstream-iframe').onload = function () { setupIframe(); } replayScreen = document.getElementById('iinfoOutstreamReplay'); iinfoOutstreamPosition = document.getElementById('iinfoOutstreamPosition'); outstreamContainer = document.getElementsByClassName('outstream-container')[0]; setupReplayScreen(); } function setupIframe() { outstreamDocument = document.getElementById('outstream-iframe').contentWindow.document; let el = outstreamDocument.createElement('style'); outstreamDocument.head.appendChild(el); el.innerText = "#adContainer>div:nth-of-type(1),#adContainer>div:nth-of-type(1) > iframe { width: 99% !important;height: 99% !important;max-width: 100%;}#videoContent,body{ width:100vw;height:100vh}body{ font-family:'Helvetica Neue',Arial,sans-serif}#videoContent{ overflow:hidden;background:#000}#adMuteBtn{ width:35px;height:35px;border:0;background:0 0;display:none;position:absolute;fill:rgba(230,230,230,1);bottom:-5px;right:25px}"; videoContent = outstreamDocument.getElementById('contentElement'); videoContent.style.display = 'none'; videoContent.volume = 1; videoContent.muted = false; if ( location.href.indexOf('rejstriky.finance.cz') !== -1 || location.href.indexOf('finance-rejstrik') !== -1 || location.href.indexOf('firmy.euro.cz') !== -1 || location.href.indexOf('euro-rejstrik') !== -1 || location.href.indexOf('/rejstrik/') !== -1 || location.href.indexOf('/rejstrik-firem/') !== -1) { outstreamDirectPlayed = true; soundAllowed = true; iinfoVastUrlIndex = 0; } if (!outstreamDirectPlayed) { console.log('OUTSTREAM direct'); setUpIMA(true); } else { if (soundAllowed) { const playPromise = videoContent.play(); if (playPromise !== undefined) { playPromise.then(function () { console.log('OUTSTREAM sound allowed'); setUpIMA(false); }).catch(function () { console.log('OUTSTREAM sound forbidden'); renderBanner(); }); } } else { renderBanner(); } } } function getWrapper() { let articleWrapper = document.querySelector('.rs-outstream-placeholder'); // Outstream Placeholder from RedSys manipulation if (articleWrapper && articleWrapper.style.display !== 'block') { articleWrapper.innerHTML = ""; articleWrapper.style.display = 'block'; } // Don't render OutStream on homepages if (articleWrapper === null) { if (document.querySelector('body.p-index')) { return null; } } if (articleWrapper === null) { articleWrapper = document.getElementById('iinfo-outstream'); } if (articleWrapper === null) { articleWrapper = document.querySelector('.layout-main__content .detail__article p:nth-of-type(6)'); } if (articleWrapper === null) { // Euro, Autobible, Zdravi articleWrapper = document.querySelector('.o-article .o-article__text p:nth-of-type(6)'); } if (articleWrapper === null) { articleWrapper = document.getElementById('sidebar'); } if (!articleWrapper) { console.error("Outstream wrapper of article was not found."); } return articleWrapper; } function setupDimensions() { outstreamWidth = Math.min(iinfoOutstreamPosition.offsetWidth, 480); outstreamHeight = Math.min(iinfoOutstreamPosition.offsetHeight, 320); } /** * Sets up IMA ad display container, ads loader, and makes an ad request. */ function setUpIMA(direct) { google.ima.settings.setDisableCustomPlaybackForIOS10Plus(true); google.ima.settings.setLocale('cs'); google.ima.settings.setNumRedirects(10); // Create the ad display container. createAdDisplayContainer(); // Create ads loader. adsLoader = new google.ima.AdsLoader(adDisplayContainer); // Listen and respond to ads loaded and error events. adsLoader.addEventListener( google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, onAdsManagerLoaded, false); adsLoader.addEventListener( google.ima.AdErrorEvent.Type.AD_ERROR, onAdError, false); // An event listener to tell the SDK that our content video // is completed so the SDK can play any post-roll ads. const contentEndedListener = function () { adsLoader.contentComplete(); }; videoContent.onended = contentEndedListener; // Request video ads. const adsRequest = new google.ima.AdsRequest(); if (direct) { adsRequest.adTagUrl = directVast; console.log('Outstream DIRECT CAMPAING advert: ' + directVast); videoContent.muted = true; videoContent.volume = 0; outstreamDirectPlayed = true; } else { adsRequest.adTagUrl = iinfoVastUrls[iinfoVastUrlIndex]; console.log('Outstream advert: ' + iinfoVastUrls[iinfoVastUrlIndex]); videoContent.muted = false; videoContent.volume = 1; } // Specify the linear and nonlinear slot sizes. This helps the SDK to // select the correct creative if multiple are returned. // adsRequest.linearAdSlotWidth = outstreamWidth; // adsRequest.linearAdSlotHeight = outstreamHeight; adsRequest.nonLinearAdSlotWidth = 0; adsRequest.nonLinearAdSlotHeight = 0; adsLoader.requestAds(adsRequest); } function setupReplayScreen() { replayScreen.addEventListener('click', function () { iinfoOutstreamPosition.remove(); iinfoVastUrlIndex = 0; outstreamInit(); }); } /** * Sets the 'adContainer' div as the IMA ad display container. */ function createAdDisplayContainer() { // We assume the adContainer is the DOM id of the element that will house // the ads. outstreamDocument.getElementById('videoContent').style.display = 'none'; adDisplayContainer = new google.ima.AdDisplayContainer( outstreamDocument.getElementById('adContainer'), videoContent); } function unmuteAdvert() { adVolume = !adVolume; if (adVolume) { adsManager.setVolume(0.3); outstreamDocument.getElementById('adMuteBtn').innerHTML = ''; } else { adsManager.setVolume(0); outstreamDocument.getElementById('adMuteBtn').innerHTML = ''; } } /** * Loads the video content and initializes IMA ad playback. */ function playAds() { // Initialize the container. Must be done through a user action on mobile // devices. videoContent.load(); adDisplayContainer.initialize(); // setupDimensions(); try { // Initialize the ads manager. Ad rules playlist will start at this time. adsManager.init(1920, 1080, google.ima.ViewMode.NORMAL); // Call play to start showing the ad. Single video and overlay ads will // start at this time; the call will be ignored for ad rules. adsManager.start(); // window.addEventListener('resize', function (event) { // if (adsManager) { // setupDimensions(); // adsManager.resize(outstreamWidth, outstreamHeight, google.ima.ViewMode.NORMAL); // } // }); } catch (adError) { // An error may be thrown if there was a problem with the VAST response. // videoContent.play(); } } /** * Handles the ad manager loading and sets ad event listeners. * @param { !google.ima.AdsManagerLoadedEvent } adsManagerLoadedEvent */ function onAdsManagerLoaded(adsManagerLoadedEvent) { // Get the ads manager. const adsRenderingSettings = new google.ima.AdsRenderingSettings(); adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true; adsRenderingSettings.loadVideoTimeout = 12000; // videoContent should be set to the content video element. adsManager = adsManagerLoadedEvent.getAdsManager(videoContent, adsRenderingSettings); // Add listeners to the required events. adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, onAdError); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, onContentPauseRequested); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, onContentResumeRequested); adsManager.addEventListener( google.ima.AdEvent.Type.ALL_ADS_COMPLETED, onAdEvent); // Listen to any additional events, if necessary. adsManager.addEventListener(google.ima.AdEvent.Type.LOADED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, onAdEvent); playAds(); } /** * Handles actions taken in response to ad events. * @param { !google.ima.AdEvent } adEvent */ function onAdEvent(adEvent) { // Retrieve the ad from the event. Some events (for example, // ALL_ADS_COMPLETED) don't have ad object associated. const ad = adEvent.getAd(); console.log('Outstream event: ' + adEvent.type); switch (adEvent.type) { case google.ima.AdEvent.Type.LOADED: // This is the first event sent for an ad - it is possible to // determine whether the ad is a video ad or an overlay. if (!ad.isLinear()) { // Position AdDisplayContainer correctly for overlay. // Use ad.width and ad.height. videoContent.play(); } outstreamDocument.getElementById('adContainer').style.width = '100%'; outstreamDocument.getElementById('adContainer').style.maxWidth = '640px'; outstreamDocument.getElementById('adContainer').style.height = '360px'; break; case google.ima.AdEvent.Type.STARTED: window.addEventListener('scroll', onActiveView); // This event indicates the ad has started - the video player // can adjust the UI, for example display a pause button and // remaining time. if (ad.isLinear()) { // For a linear ad, a timer can be started to poll for // the remaining time. intervalTimer = setInterval( function () { // Example: const remainingTime = adsManager.getRemainingTime(); // adsManager.pause(); }, 300); // every 300ms } outstreamDocument.getElementById('adMuteBtn').style.display = 'block'; break; case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: if (ad.isLinear()) { clearInterval(intervalTimer); } if (outstreamLastError === 303) { if (isBanner) { renderBanner(); } else { replayScreen.style.display = 'flex'; } } break; case google.ima.AdEvent.Type.COMPLETE: // This event indicates the ad has finished - the video player // can perform appropriate UI actions, such as removing the timer for // remaining time detection. if (ad.isLinear()) { clearInterval(intervalTimer); } if (isBanner) { renderBanner(); } else { replayScreen.style.display = 'flex'; } break; } } /** * Handles ad errors. * @param { !google.ima.AdErrorEvent } adErrorEvent */ function onAdError(adErrorEvent) { // Handle the error logging. console.log(adErrorEvent.getError()); outstreamLastError = adErrorEvent.getError().getErrorCode(); if (!loadNext()) { renderBanner(); } } function renderBanner() { if (isBanner) { console.log('Outstream: Render Banner'); iinfoOutstreamPosition.innerHTML = ""; iinfoOutstreamPosition.style.height = "330px"; iinfoOutstreamPosition.appendChild(bannerDiv); } else { console.log('Outstream: Banner is not set'); } } function loadNext() { iinfoVastUrlIndex++; if (iinfoVastUrlIndex < iinfoVastUrls.length) { iinfoOutstreamPosition.remove(); outstreamInit(); } else { return false; } adVolume = 1; return true; } /** * Pauses video content and sets up ad UI. */ function onContentPauseRequested() { videoContent.pause(); // This function is where you should setup UI for showing ads (for example, // display ad timer countdown, disable seeking and more.) // setupUIForAds(); } /** * Resumes video content and removes ad UI. */ function onContentResumeRequested() { videoContent.play(); // This function is where you should ensure that your UI is ready // to play content. It is the responsibility of the Publisher to // implement this function when necessary. // setupUIForContent(); } function onActiveView() { if (outstreamContainer) { const containerOffset = outstreamContainer.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight/1 && containerOffset.bottom > 0.0) { if (outstreamPaused) { adsManager.resume(); outstreamPaused = false; } return true; } else { if (!outstreamPaused) { adsManager.pause(); outstreamPaused = true; } } } return false; } let outstreamInitInterval; if (typeof cpexPackage !== "undefined") { outstreamInitInterval = setInterval(tryToInitializeOutstream, 100); } else { const wrapper = getWrapper(); if (wrapper) { let outstreamInitialized = false; window.addEventListener('scroll', () => { if (!outstreamInitialized) { const containerOffset = wrapper.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight / 1 && containerOffset.bottom > 0.0) { outstreamInit(); outstreamInitialized = true; } } }); } } function tryToInitializeOutstream() { const wrapper = getWrapper(); if (wrapper) { const containerOffset = wrapper.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight / 1 && containerOffset.bottom > 0.0) { if (cpexPackage.adserver.displayed) { clearInterval(outstreamInitInterval); outstreamInit(); } } } else { clearInterval(outstreamInitInterval); } } }
OSZAR »