SSL není bezpečné, ukazuje případ Comodo

4. 4. 2011
Doba čtení: 8 minut

Sdílet

Ilustrační obrázek
Autor: Depositphotos – stori
Ilustrační obrázek
Internetové vody rozvířil případ společnosti Comodo, jejíž napadení umožnilo do světa rozeslat podvržené SSL certifikáty. Ty mohou sloužit pro prakticky neodhalitelné man-in-the-middle útoky. Jeden bezpečnostní incident ukázal několik výrazných slabin celého SSL (HTTPS) a jeho principiálních děr.

Následující případ krásně ukázal, jak je možné z konkrétního vyvodit obecné. Jeden poměrně běžný bezpečnostní incident jedné firmy totiž ukázal na slabiny systému jako celku a dokázal, že i SSL má své velké principiální slabiny. Abychom pochopili celý problém, je třeba pochopit princip SSL (znalci prominou).

Šifruji, tedy jsem

Aby nebylo možné jednoduše na síti odposlouchávat běžící informace, je potřeba je před odesláním do prostoru zašifrovat. Dnes se k tomu nejčastěji používají asymetrické šifry, které zajišťují, že přenos šifrovacích klíčů nechráněnými kanály není žádným problémem. Veřejný klíč se veřejným kanálem dostane ke druhé straně a ta s ním šifruje komunikaci, která může být rozšifrována soukromým klíčem, který nikdy neopustil své původní místo.

Tento princip je dobře znám a jeho výhodou je především pohodlné předávání šifrovacích klíčů, které nevyžaduje žádná další bezpečnostní opatření. Všechny potřebné kroky k zahájení komunikace řeší SSL, ale kromě toho se zabývá také další podstatnou věcí – autentizací.

V praxi nám totiž nestačí „jen tak šifrovat“, ale musíme vědět, jestli jsme skutečně obdrželi veřejný klíč toho, s kým jsme si přáli komunikovat. Pokud bychom autentizaci nezajistili, kdokoliv na trase by se mohl vydávat za naši protistranu, podstrčit nám své klíče a my bychom zahájili komunikaci s ním. Pokud by dokázal během následujícího rozhovoru úspěšně předstírat, že je někdo jiný, dokázal by z nás získat všechny informace.

V praxi se takové útoky provádějí například na uživatele bank nebo e-mailových účtů. Útočník se postaví mezi obě strany a uživateli na vyžádání ukáže přihlašovací stránku, naprosto shodnou s tou, kterou používá banka. Uživatel do ní vloží své přihlašovací údaje a nevědomky je tak předá útočníkovi. Ten na pozadí naváže spojení se skutečnou bankou a klidně uživatele přihlásí a předá mu odpověď od banky. Uživatel tak vůbec nezaznamená, že se stalo něco podezřelého.

Všichni věříme jemu, on věří mě

Aby bylo možné autentizaci v SSL provádět, byl vymyšlen takzvaný řetězec důvěry. Zjednodušeně jde o to, že byly stanoveny jisté „certifikační autority“, kterým uživatelé věří. Tyto autority mají mandát v ověřování dalších subjektů a přidělování certifikátů, které slouží jako doklad pravosti. Vlastně říkají „já, autorita, potvrzuji, že tento subjekt je pravý“.

Poměrně dobře se dají certifikáty přirovnat k občanským průkazům. V takové představě pak český stát vystupuje jako certifikační autorita, která na základě ověření vystavuje doklad o pravosti. Pokud nám někdo takový doklad předloží, my jej považujeme za pravý, protože je vydaný autoritou, které důvěřujeme (teoreticky). Samozřejmě si takový doklad může v zásadě vyrobit kdokoliv, ale tím pro nás tento doklad ztratí na důvěryhodnosti, protože jej nevystaví důvěryhodná autorita.

Přesně tímto způsobem funguje i SSL. Každá aplikace má k dispozici seznam „kořenových certifikátů“ – tedy certifikátů certifikačních autorit. Ty v nich potvrzují samy sebe a aplikace ve vašem operačním systému automaticky těmto autoritám věří. Pokud se setkají s jakýmkoliv dalším certifikátem, zkontrolují, zda jej vystavil někdo, komu věří (jehož kořenový certifikát je k dispozici a označen jako důvěryhodný). Pokud ano, je zkoumaný certifikát bez řečí přijat, protistrana je autentizovaná a může se začít komunikovat.

Aby vůbec bylo možné takový certifikát vystavit, žadatel se musí autoritě prokázat reálnými dokumenty, které dokazují jeho pravost. Obvykle je třeba odeslat notářsky ověřené dokumenty, u vyšších typů certifikátů je prověřování ještě složitější. Platí tedy: pokud nedokážeš, že jsi pravý, my ti potvrzení (certifikát) nevystavíme. Získat falešný certifikát je tak poměrně obtížné (i když ne nemožné).

Součástí certifikátu jsou navíc další informace o platnosti, o vystaviteli, o certifikovaném subjektu a podobně. Poměrně snadno je tak možné ověřit, kdo příslušné potvrzení vydal a na „čí jméno“ je vystaveno. Uživatel se tak může v případě podezření snadno ujistit, že je vše v pořádku, šifrování probíhá a protistrana je tím, za koho se vydává.

Případný podvodník si sice může vytvořit svůj vlastní certifikát, který si sám podepíše (self-signed), ale už není schopen si vytvořit certifikát potvrzený důvěryhodnou autoritou. Tím je zajištěna bezpečnost celé komunikace včetně autentizace. Na tomto prvku stojí celé SSL.

Případ Comodo

Bohužel se ukazuje, že tento princip stojí na jediném pilíři – certifikačních autoritách. Pokud totiž některá z nich začne vydávat platné certifikáty někomu, komu by neměla, celý výše popsaný systém zkolabuje. Vše závisí na tom, zda mechanismy autority fungují správně, zda neexistují žádné bezpečnostní mezery a zda není možné zneužít žádnou technickou chybu.

Autority tak vytvářejí kritický prvek selhání (single point of failure), protože se jednoduše doufá v to, že jsou nenapadnutelné a vše v nich funguje zcela správně. Tedy, že nevystavují certifikáty někomu cizímu. A přesně to společnost Comodo udělala – devětkrát.

Firma 15. března přiznala, že došlo k vážnému narušení bezpečnosti. Přes účet jednoho z partnerů (InstantSSL.it, později byli kompromitováni další dva) byl založen účet nový a na ten bylo vystaveno devět neověřených certifikátů. Celkem se jedná o certifikáty vystavené na sedm domén:

  • mail.google.com
  • login.live.com
  • www.google.com
  • login.yahoo.com (tři certifikáty)
  • login.skype.com
  • addons.mozilla.org
  • global trustee

Útoky přišly z různých IP adres, ale především z Íránu (IP adresa 212.95.136.18), útočník byl velmi dobře připravený, rychle napadl systém, rychle vytvořil potřebné certifikáty a zmizel. Útočník požádal celkem o devět certifikátů, ale Comodo netuší, zda si všechny vyzvedl. Jisté je, že certifikát na login.yahoo.com byl v praxi použit.

Později se k útoku přihlásil člověk pod přezdívkou ComodoHacker, který na Pastebin odeslal důkazy o svém činu. Kromě jiného vylíčil princip útoku, který byl velmi jednoduchý – podařilo se mu uhádnout heslo zmíněného partnera firmy Comodo. Uživatelské jméno bylo gtadmin a heslo globaltrust. Školácká chyba na takto kritickém místě mu umožnila vstup do systému.

Na serveru pak našel knihovnu, která se starala o vyřizování žádostí u Comodo a pomocí dekompilace z ní získal příslušná hesla k účtům. Pak už mu nic nebránilo v odeslání vlastních žádostí o certifikáty. Pravost útočníka je předmětem debat, podstatné ale je, že se „něco podobného“ zcela jistě stalo.

Útočník se tak může za pomocí certifikátů vydávat za kterýkoliv ze zmíněných sedmi serverů a počítač oběti to nepozná. I přes použití šifrování tak bude veškerá komunikace pro útočníka otevřená a on bude moci dešifrovat uživatelská jména, hesla, ale i obsah přenášených informací. Skrze podvržený server addons.mozilla.org je možné například nainstalovat vlastní rozšíření obsahující zákeřný kód. Možnosti zneužití jsou velmi široké.

Revokace neplatných certifikátů

SSL je poměrně chytře navržený systém, který počítá i s takovou alternativou. Umí se vyrovnat s takto fatálním únikem pomocí velmi jednoduchého mechanismu. Každý kořenový certifikát zabudovaný do operačního systému nebo třeba webového prohlížeče obsahuje URL, na které je k nalezení seznam revokovaných certifikátů (Certificate Revocation List, CLR). Na tento seznam jsou umístěny certifikáty, které autorita považuje za neplatné a klienti už jej nemají nadále používat.

Každá aplikace, která provádí ověření přijatého certifikátů, je povinna si tento seznam stáhnout a prověřit, zda na něm zkoumaný certifikát není. Pokud ano, je certifikát automaticky odmítnut a už nikdy nebude považován za platný. Až potud se může zdát, že revokační mechanismus je dobře navržený a velmi účinný, ale má jednu výraznou slabinu.

Pokud totiž při ověření dostane počítač informaci o tom, že server s revokačním seznamem je nedostupný (500 interní chyba), nezareaguje dočasným zablokováním certifikátů, ale naopak kontrolu ukončí a považuje certifikáty automaticky za platné. Pokud může útočník manipulovat na síti s jedním serverem a vydávat se za něj, může stejně tak pro uživatele vyřadit z provozu jiný server a zabránit tak zneplatnění svého těžce nabytého certifikátu.

Toto je samozřejmě známý fakt a tvůrci nejohroženějšího software – webových prohlížečů – se s klasickou revokací samozřejmě nespokojili a nasadili vlastní protiopatření. Vývojáři Firefoxu 4 vydali neplánovanou RC verzi, která natvrdo zablokovala zmíněné certifikáty a podobně postupoval také Google s Chrome. Pokud by se některý z falešných certifikátů objevil, prohlížeč jej automaticky podle sériového čísla zamítne. Také Microsoft už vydal potřebnou záplatu pro všechny podporované operační systémy. Podstatně horší to ale bude například u mobilních zařízení, kde často není dostupná automatická a jednoduchá aktualizace a vše je ponecháno na uživateli, který by proto měl aktualizovat systém ve svém přístroji sám.

Když je důvěra zrazena

Není to poprvé, kdy se objevil podobný problém s vystavováním certifikátů. V roce 2001 se například neznámému útočníkovi podařilo z VeriSignu vylákat certifikáty vydané na Microsoft a mohl s nimi pak podepisovat například podvržené aplikace. Objevily se i další nedostatky v řetězci důvěry, které ohrožují bezpečnost. Tento útok je ovšem svou podstatou dalekosáhlejší než ty předchozí. Kvůli problémům s revokací se budeme s jeho následky setkávat ještě dlouhou dobu.

Ukazuje se tak, že absolutní předání důvěry certifikačním autoritám není bezpečné a vytváří řadu potenciálních bodů selhání. V takové situaci pak stačí prolomit jediný z nich a celý systém naprosto selhává. Vzniká tak situace, kdy celý internet bezmezně důvěřuje několika firmám, jejichž bezpečnost může být kdykoliv narušena.

Pravděpodobně tak vznikne tlak na nahrazení stávajícího absolutistického systému důvěry něčím méně závislým na jednotlivých bodech. Do úvahy tak přichází web of trust známý z PGP/GPG nebo třeba umisťování certifikátů do DNS záznamů, které komplikuje útok na konkrétní domény. Další možností jsou krátkodobé certifikáty, které bude potřeba pravidelně na serverech automaticky obměňovat. Pokud dojde k podobnému incidentu, uživatelský počítač bude automaticky upozorněn na problém.

Tento případ ukazuje, že situace není dlouhodobě udržitelná a je potřeba ji změnit. I to však bude běh na dlouhou trať a objeví se řada technických překážek. Zároveň se budou autority bránit jakékoliv změně, protože aktuální stav jim přináší nemalé zisky z jejich podnikání a narušení „monopolu na důvěru“ by pro ně znamenalo pochopitelnou ztrátu.

Odkazy

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í.

'; 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 »