Moderní DNSSEC: eliptické křivky a nevinné lži

11. 1. 2016
Doba čtení: 7 minut

Sdílet

Ilustrační obrázek
Autor: Depositphotos – stori
Ilustrační obrázek
Systém bezpečných doménových jmen DNSSEC je tu s námi v ostrém provozu už víc než pět let. Ačkoli základní principy zůstávají stejné, prochází nadále inovacemi. V tomto článku se zaměříme na podporu eliptických křivek v DNSSECu a na efektivní implementaci negativních odpovědí při online podepisování.

Podpora eliptických křivek v DNSSECu

Algoritmy založené na eliptických křivkách jsou aktuálním módním trendem v kryptografii, a tak není divu, že ani DNSSEC se jim nevyhýbá. Mezi hlavní výhody eliptických křivek patří rychlejší generování podpisů a především pak podstatně kratší délka klíče při stejné síle. Jak je možné ověřit například na serveru Keylength.com, ECDSA klíč délky 256 bitů poskytuje bezpečnost odpovídající RSA klíči délky kolem 3072 bitů. Takový klíč je tedy pravděpodobně bezpečnější i než 2048bitový RSA klíč samotné kořenové zóny. Malá velikost klíče i podpisů je obecně pro DNS výhodná, neboť s narůstáním objemu DNS zpráv dochází k jistým provozním problémům, jako je fragmentace, přechod na transportní protokol TCP nebo nárůst zesilovacího faktoru při kybernetických útocích.

Podporu pro eliptické křivky v DNSSECu zavádí RFC 6605, které vyšlo již v roce 2012. Konkrétně jsou definovány dva algoritmy a sice ECDSAP256SHA256 s číslem 13 a ECDSAP384SHA384 s čílem 14. Jak názvy napovídají, jedná se o použití eliptických křivek P-256, resp. P-384 podle specifikace FIPS 186–3 a hashovací funkce SHA256, resp. SHA384.

Jedinou, zato ale zásadní, nevýhodou použití ECDSA algoritmu v DNSSECu je jeho nepodpora staršími validátory. Ta vychází zejména z nejistoty ohledně existence patentů, kvůli které některé distribuce podporu eliptických křivek v OpenSSL záměrně vypínaly. Ještě v roce 2014 George Michaelson z laboratoří APNIC před jejich použitím varoval. Od té doby však nastal velký pokrok, validaci ECDSA podpisů zvládne ve výchozím nastavení většina aktuálních linuxových distribucí. Důležité také je, že i při nepodpoře ECDSA algoritmu validátorem nedojde k žádnému fatálnímu selhání, validátor zónu propustí bez validace stejně, jako by nebyla podepsána vůbec.

O tom, jak je na tom váš DNS resolver s podporou různých DNSSEC algoritmů se můžete snadno přesvědčit pomocí utilitky alg_rep. Ta provede úplný test všech definovaných DNSSEC algoritmů proti všem typům DS záznamů a oběma typům NSEC záznamů. Výsledek je zobrazen v přehledné tabulce:

Zone dnssec-test.org.  Qtype DNSKEY Resolver [2001:4860:4860::8888]
DS     :  1  2  3  4  |  1  2  3  4
ALGS   :    NSEC      |     NSEC3
alg-1  :  S  S  S  S  |  x  x  x  x  => RSA-MD5 OBSOLETE
alg-3  :  -  -  -  -  |  x  x  x  x  => DSA/SHA1
alg-5  :  V  V  -  V  |  x  x  x  x  => RSA/SHA1
alg-6  :  x  x  x  x  |  -  -  -  -  => DSA-NSEC3-SHA1
alg-7  :  x  x  x  x  |  V  V  -  V  => RSA-NSEC3-SHA1
alg-8  :  V  V  -  V  |  V  V  -  V  => RSA-SHA256
alg-10 :  V  V  -  V  |  V  V  -  V  => RSA-SHA512
alg-12 :  -  -  -  -  |  -  -  -  -  => GOST-ECC
alg-13 :  V  V  -  V  |  V  V  -  V  => ECDSAP256SHA256
alg-14 :  V  V  -  V  |  V  V  -  V  => ECDSAP384SHA384
V == Validates  - == Answer  x == Alg Not specified
T == Timeout S == ServFail O == Other Error
DS algs 1=SHA1 2=SHA2-256 3=GOST 4=SHA2-384 

V ideálním případě budou ve všech polích tabulky znaky V, značící úspěšnou validaci, případně x pro neexistující kombinace algoritmu a NSEC záznamu. Pokud je v poli znak -, znamená to, že validátor daný podpis nepodporuje a data propustil bez ověření. Znak S pak značí návratový kód selhání serveru. Ve výše uvedeném příkladu takovou odpověď vrací zóny podepsané zastaralým protokolem RSA-MD5.

Nevinné lži při online podepisování

Zásadním návrhovým požadavkem, který do velké míry ovlivnil výsledný návrh protokolu DNSSEC, byla podpora offline podepisování, tedy to, aby autoritativní DNS server mohl pracovat zcela bez přístupu k privátnímu klíči, pomocí kterého jsou podpisy vytvářeny. Splnění tohoto požadavku spolu s podporou pro tzv. žolíkové DNS záznamy a odolností proti útokům přehráním zaznamenané komunikace si vyžádalo poměrně komplexní systém NSEC záznamů, který byl ještě dále zkomplikován přechodem na NSEC3 záznamy za účelem znesnadnění procházení zónového souboru postupnými dotazy.

Důsledkem je, že zatímco pozitivní odpověď na DNS dotaz je přímočará – k dotazovaným záznamům se jednoduše připojí podpis – negativní odpověď musí obsahovat mnohem víc dat: kromě tradičního SOA záznamu a jeho podpisu ještě NSEC záznam prokazující, že dotazované jméno neexistuje a NSEC záznam prokazující, že neexistuje ani žolíkový záznam, který by pokrýval dotazované jméno, oba záznamy ještě doprovázené podpisy. Při použití NSEC3 jsou takové záznamy dokonce tři. Stejně tak jsou komplikované i pozitivní odpovědi vzniklé z žolíkových záznamů, kde je třeba kromě podepsané odpovědi doručit i podepsaný NSEC důkaz, že opravdu neexistuje lepší záznam a bylo nutné použít žolík. Celou problematiku hezky shrnuje informativní RFC 7129. Důsledkem takto složitého systému je kromě nárůstu délky zpráv také spousta implementačních chyb, způsobujících například problémy při řetězení resolverů a validaci žolíkových záznamů.

Offline podepisování však není jediný možný způsob nasazení DNSSEC. Online podepisování může být výhodné jak pro menší instalace, tak i pro velké sítě CDN, které obsah DNS odpovědi mění v reálném čase na základě nejrůznějších faktorů. Zde je třeba zdůraznit, že přítomnost privátního klíče na DNS serveru není nic strašidelného; úplně stejně fungují snad všechny kryptografické protokoly, které znáte, namátkou třeba HTTPS nebo SSH. Přítomnost privátního klíče pak umožní zásadním způsobem zjednodušit obsah negativních odpovědí, protože odpověď obsahuje přímo podepsaný dotaz.

Tento postup se obecně nazývá NSEC/NSEC3 white lies, česky tedy nevinné lži. Zatímco v při tradičním podepisování by dotaz na neexistující jméno jablko vrátil NSEC záznam s nejbližšími existujícími záznamy, třeba banán NSEC pomeranč, v režimu nevinných lží server vygeneruje minimální NSEC záznam, pokrývající pouze dotazované jméno, například tedy jablkn NSEC jablkp. Takový postup implementoval samotný Dan Kaminsky ve svém DNSSEC proxy serveru Phreebird a dočkal se i standardizace v RFC 4470.

S ještě zajímavější variantou nevinných lží přišla společnost CloudFlare. Jejich implementace online podepisování je zvláštní tím, že na jakýkoli dotaz odpoví pozitivně, tedy s návratovým kódem NOERROR. Pokud dotazovaná data neexistují, raději předstírají, že dané jméno sice existuje, ale nemá žádný záznam dotazovaného typu. Tímto způsobem, obsahuje negativní odpověď pouze jeden SOA a jeden NSEC záznam. Spolu s použitím ECDSA algoritmu je tak možné i negativní odpovědi stlačit pod délku zprávy 512 bajtů a ušetřit tak případné problémy s velkými DNS zprávami.

Podepisování jedním klíčem

Od počátku DNSSECu bylo doporučováno používání dvojice klíčů pro podepisování zónového souboru. Slabší klíč ZSK podepisuje všechny záznamy v zóně, silnější klíč KSK podepisuje pouze záznam DNSKEY s veřejnou částí klíče ZSK. Otisk klíče KSK je publikován v DS záznamu nadřazené zóny, čímž je sestaven řetěz důvěry.

Vizualizace hierarchie KSK a ZSK klíčů nástrojem DNSViz.net

Tímto způsobem se řeší požadavek na dostatečně krátké podpisy, aby objem DNS dat příliš nenarostl, zároveň s protichůdným požadavkem na dostatečně bezpečné klíče, aby je nebylo nutné měnit příliš často, neboť každá výměna vyžaduje (typicky manuální) komunikaci s operátorem nadřazené zóny. Obvyklá současná praxe je použití 1024bitového RSA klíče jako ZSK a 2048bitového RSA klíče jako KSK, přičemž ZSK je obvykle měněn automaticky každý měsíc, KSK manuálně po roce, dvou, nebo vůbec. Zajímavé je to zejména v kontrastu s TLS certifikáty, kde ještě nedávno existovaly kořenové certifikáty používající 1024bitové RSA klíče, vydané s platností 10 let.

Příchod eliptických křivek umožňuje i správu klíčů výrazným způsobem zjednodušit. 256bitový ECDSA klíč je silnější než běžné současné KSK klíče a zároveň generuje podpisy kratší než současné ZSK klíče. Odpadá zde tedy požadavek na častou výměnu ZSK klíče a nic nebrání podepisování celé zóny jedním klíčem, který stačí manuálně jednou za čas vyměnit.

Používání původní dvojce samostatných KSK a ZSK klíčů samozřejmě není zakázáno. Třeba dříve zmíněná společnost CloudFlare používá dvojici klíčů z důvodu bezpečnosti – ZSK klíče jsou přítomny přímo na DNS serverech ve všech uzlech, zatímco KSK klíče jsou v bezpečném kontrolovaném prostředí. V případě kompromitace ZSK klíče je možné tento vyměnit a revokovat, aniž by to vyžadovalo komunikaci s nadřazenou zónou.

prace_s_linuxem_tip

Další křivky na obzoru

Standardizované křivky P.256 a P.384, které jsou v současné době jediné dvě podporované v DNSSECu, byly kryptology Danielem J. Bernsteinem a Tanjou Lange v nedávném výzkumu označeny jako nebezpečné. Zřejmě se nejedná se o nějaké bezprostřední riziko, rozhodně však není od věci podporovat i jiné křivky, takové, které zmiňovaná zpráva označuje jako bezpečné.

Jednou z nich je i Bernsteinova křivka Curve25519. Podporu pro její použití v DNSSECu v tuto chvíli navrhuje v IETF Ondřej Surý, stejně jako další křivku Ed448. Není však možné očekávat reálnou nasaditelnost těchto nových křivek dříve než za několik let.

Autor článku

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 »