Cloudbleed: únik dat sdíleného proxy serveru

27. 2. 2017
Doba čtení: 6 minut

Sdílet

Ilustrační obrázek
Autor: Depositphotos – stori
Ilustrační obrázek
Společnost Cloudflare oznámila chybu ve své infrastruktuře, jejíž následky jsou velmi podobné zranitelnosti Heartbleed v OpenSSL. Díky podrobné zprávě o incidentu si můžeme přečíst, co se přesně stalo.

Začalo to celé nenápadně, v pátek 17. února odpoledne, kdy bezpečnostní výzkumník z Google, Tavis Ormandy, napsal na Twitter status, který předznamenával něco velkého.

Během analýzy výsledků tzv. fuzzingu, tedy testování kódu velkou množinou různých vstupů, narazil na podivný webový obsah, který obsahoval evidentně kusy neinicializované paměti. Záhy se ukázalo, že problém je v proxy serverech služby Cloudflare. Když se mu ho podařilo izolovat a reprodukovat, snažil se jej co nejrychleji předat bezpečnostnímu týmu Cloudflare.


Tavis Ormandy

Ukázka uniklých dat – data známé aplikace Uber

První pomoc

Bezpečnostní tým Cloudflare okamžitě pochopil, že jde o vážnou situaci. První, co udělal, bylo vypnutí doplňkových služeb, které nejspíše problém způsobovaly. Konkrétně šlo o služby obfuskace e-mailových adres, server-side excludes a automatických přepisů odkazů z http  na https. Podle zprávy o incidentu byly také okamžitě sestaveny týmy pro řešení incidentu – jeden v San Franciscu a druhý v Londýně tak, aby se mohly střídat po 12hodinových směnách v nepřetržitém provozu. K vypnutí e-mailové obfuskace, která způsobovala nejvíce úniků, došlo už 47 minut po nahlášení, ke kompletnímu vypnutí všech funkcí, které únik způsobovaly, došlo 7 hodin po nahlášení.

Hledání příčiny

Od začátku bylo zřejmé, že chyba je ve funkcích, které v proxy serverech za běhu upravují HTML kód stránek. K tomuto účelu v Cloudflare dlouho používali vlastní parser napsaný v jazyce Ragel. Před časem však došli k závěru, že tento kód je příliš složitý a těžko udržovatelný, a tak začali vyvíjet nový, cf-html. Oba parsery jsou provedeny jako moduly pro webový server NGINX a během přechodného období jsou aktivní oba.

Další vyšetřování ukázalo, že chyba byla ve starém kódu přítomna mnoho let, teprve kombinace s novým modulem ji však dokázala vyvolat. Přímou příčinou byla nedostatečná kontrola přetečení ukazatele za konec řetězce v kódu, generovaném kompilátorem jazyka Ragel:

/* generated code */
if ( ++p == pe )
    goto _test_eof; 

Je třeba zdůraznit, že nejde o chybu v kompilátoru jazyka Ragel, ale o chybu ve zdrojovém kódu v tomto jazyce, jehož autorem je Cloudflare.

Podmínka ve výše uvedeném kódu kontroluje pouze rovnost s koncovou zarážkou. Dojde-li z nějakého důvodu k přeskočení ukazatele za zarážku, není konec vstupu detekován a program čte z paměti bezprostředně následující za bufferem. V této paměti se mohou nacházet různá data z předchozích komunikací. Jelikož je infrastruktura Cloudflare sdílená mezi různými zákazníky, je možné získat data i jiných webových stránek, než těch, které jsou problémem postiženy. Princip čtení přes hranice alokované paměti je velmi podobný chybě Heartbleed z roku 2014.


Tavis Ormandy

Ukázka uniklých dat – data fitness náramku Fitbit

Spící zranitelnost

Chyba čtení za hranice byla v kódu přítomna nejspíše od samého počátku. K jejímu vyvolání došlo vyvoláním chyby parseru na samém konci posledního bufferu, například, když HTML kód na zdrojovém serveru končil takovouto neukončenou značkou:

<script type= 

Chyba se však nemohla projevit, dokud tento modul pracoval v NGINX samostatně. Je to způsobeno stylem, jakým NGINX předává modulu data. Teprve v okamžiku, kdy byl k původnímu modulu přidán nový cf-html, byly splněny podmínky k tomu, aby chyba v původním kódu začala škodit.

K prvnímu nasazení cf-html došlo 22. září 2016, kdy byla do cf-html zmigrována funkce automatického přepisování http na https. Zákazníci, kteří měli tuto funkci zapnutou a zároveň splnili podmínku nevalidně ukončeného HTML, mohli způsobovat únik dat už od té doby. Tato funkce však není podle slov Cloudflare příliš používaná.

Dalším nasazením cf-html byla funkce Server-Side Excludes, k jejíž migraci došlo 30. ledna 2017. Tato funkce je však aktivována pouze pro IP adresy se špatnou reputací a slouží k filtrování potenciálně citlivých údajů. Pro běžný provoz se neprojeví. Největší vliv tak měla zatím poslední ze série migrací, která proběhla 13. února, tedy pouhých pár dní před zjištěním incidentu. Jednalo se o migraci funkce obfuskace e-mailových adres, která je naopak používána velmi často.

Identifikace poškození

Cloudflare provozuje pro různé úrovně zpracování webového obsahu samostatné instance webserveru NGINX. Proces, ve kterém byla chyba, je součástí zpracování HTML a je zcela oddělen od procesů terminace TLS, rekomprese obrázků a kešování. Je tedy jisté, že touto zranitelností nemohlo dojít ke kompromitaci privátních klíčů od zákaznických certifikátů. Mohlo však dojít k vyzrazení šifrovacích klíčů, kterými Cloudflare šifruje komunikaci mezi jednotlivými servery v rámci datacentra. Toto šifrování bylo zavedeno v reakci na informace Edwarda Snowdena o masivním monitorování.

Největším problémem ale je únik částí HTTP komunikace jiných zákazníků, kteří používali stejný proxy server. Taková komunikace může obsahovat uživatelská jména a hesla, nebo přinejmenším cookie sezení, které je možné zneužít ke kompromitaci cizích identit.

Zamořené keše

Zásadním problémem také je, že k vyvolání úniku dat nebyla zapotřebí (na rozdíl třeba od Heartbleedu) žádná sofistikovaná činnost, stačilo pouze stahovat webové stránky. To je činnost, která je bezpochyby nejčastější internetovou aktivitou vůbec a kromě koncových uživatelů ji provádějí automaticky nejrůznější roboti.

Uniklá data se tak objevila v keších všemožných vyhledávačů a webových archivů. Společnost Cloudflare vyjednala s několika vyhledávači odstranění podobných dat, můžeme se však jen dohadovat, zda se všechna data najít povedlo a zda někde stále neleží. Je celkem pravděpodobné, že nějaké úniky budou k nalezení i v lokálních keších webových prohlížečů v počítačích a mobilech celého světa.

Uniklá data na prodej

Teprve po nasazení nové verze HTML parserů s opravenou zranitelností, ke kterému došlo večer v úterý 21. února, a odstranění všech nalezených úniků z výsledků vyhledávání, byla informace o zranitelnosti zveřejněna. Kromě již zmíněné post mortem analýzy na blogu Cloudflare byl také zpřístupněn tiket, ve kterém Tavis Ormandy dokumentoval postup opravy problému, včetně ukázek úniků. Společnost Cloudflare také rozseslala všem zákazníkům hromadný e-mail, ve kterém upozorňuje na to, že postižených bylo pouze přibližně 150 zákazníků, nicméně doporučuje zneplatnit a vyměnit všechna dlouhodobá tajemství, jakými jsou třeba cookie sezení.

Počet 150 zákazníků se zdá velmi podhodnocený, neboť jde pouze o počet unikátních doménových jmen, ve kterých byly nalezené uniklé informace prostřednictvím vyhledávačů. Není přitom zřejmé, zda se bezpečnostním expertům podařilo identifikovat, komu patřila vlastní uniklá data. Každý, kdo provozuje webserver za Cloudflare proxy, by tedy měl minimálně zrušit všechna uložená sezení. Ideálně pak také vyzvat uživatele k preventivní změně hesla. Ostatně, netrvalo dlouho a temný web začal nabízet k prodeji soubory uniklých dat. Těžko říct, zda jde o skutečný únik nebo o jednoduchý podvod, přiživující se na aktuální zprávě.

Zneplatněná přihlášení Google s problémem nesouvisí

Shodou okolností minulý týden Google zneplatnil uložená přihlášení velké části uživatelů. Vypadá to jako souvislost, ale nedává to úplně smysl, protože Google služby určitě Cloudflare nepoužívají a je tedy nepravděpodobné, že by cookies, které slouží k autentizaci vůči Google, byly v jakémkoli nebezpečí. Tavis Ormandy na přímý dotaz odpovídá, že incidenty nemají nic společného, na fóru Google je pouze zpráva, že v průběhu rutinní údržby došlo k odhlášení některých uživatelů. Nejspíš tedy opravdu jde o pouhou shodu okolností.

prace_s_linuxem_tip

Maximální otevřeností k minimalizaci škod

Na celé události je zajímavý především způsob, jakým společnost Cloudflare o problému informovala. Přestože se jedná o komerční firmu, a chyba se objevila v proprietárním softwaru, který je součástí firemního know-how, množství informací zásadně překračuje obvyklé strohé sdělení typu: „V našich systémech se vyskytla chyba, už jsme ji opravili, změňte si prosím heslo.“

Překvapující je také rychlost, s jakou byla společnost schopna úniky dat zastavit (i za cenu omezení služeb) i jak rychle byla nainstalována oprava. Škoda jen, že zpráva nejspíše podceňuje počet obětí. V haldě podrobných technických informací se také ztrácí krátká a jednoduchá informace pro zákazníky, co mají se svou službou za Cloudflare dělat, aby vliv případných úniků minimalizovali.

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 »