Internet hloupých věcí

13. 7. 2015
Doba čtení: 9 minut

Sdílet

Autor: Shutterstock
Od internetových prognostiků často slýcháme, že se chystá nespoutaná budoucnost představovaná „internetem věcí“. Je tím myšlen několik dekád trvající přechod počítačového průmyslu od velkých tajemných zařízení ke stolním, přenosným, kapesním a dnes i náramkovým počítačům. Kam bude vývoj pokračovat dál?

Článek je překladem zápisku The Internet of Stupid Things, který napsal v dubnu 2015 Geoff Huston. Překlad vychází se souhlasem původního autora.

Vize internetu věcí není vůbec nová. Moje auto má už dnes asi 100 mikroprocesorů, které se starají o všechno od regulace motoru až po ukládání polohy sedačky. Ambiciózní vize internetu věcí předpokládá propojení všech těchto procesorů do jednoho ohromného internetu. Gartner předpokládá, že svět chatujícího křemíku naroste na 25 miliard zařízení do roku 2020. Cisco sázku zvyšuje na 50 miliard, a Morgan Stanley trumfuje s předpokladem 75 miliard zařízení připojených k internetu do stejného roku. Ostatní jdou se svými odhady klidně až na 100 miliard. Taková míra nespoutané technické euforie počítá s ekonomickou hodnotu aktivit v této oblasti v řádu bilionů dolarů v roce 2020.

Co ale víme o věcech, které jsou k internetu připojené už nyní?

Některé z nich nejsou moc dobré. Přesněji řečeno, některé z nich jsou opravdu hloupé. A jejich hloupost je nebezpečná, protože způsob chování a přístup k bezpečnosti často škodlivým způsobem ovlivňuje ostatní.

Jeden z dřívějších incidentů způsobených hloupostí ve velkém měřítku se odehrál v roce 2003, když Dave Plonka, tehdy z University of Wisconsin-Madison, zpozoroval něco, co vypadalo jako obrovský útok namířený na univerzitní časový server. Jak bylo tehdy napsáno:

V květnu 2003 se University of Wisconsin-Madison stala příjemcem soustavného zaplavování internetovým provozem, který byl cílen na jeden z veřejných časových (NTP) serverů. Míra útoku byla ve stovkách tisíc paketů za sekundu a ve stovkách megabitů za sekundu.

Následně jsme zjistili že zdrojem provozu byly doslova stovky tisíc skutečných internetových zařízení z celého světa. Příčina nicméně nespočívala ve zlomyslném distribuovaném útoku (DDoS), ale v závažné návrhové chybě ve stovkách tisíc levných internetových produktů jednoho výrobce, které byly určeny pro domácnosti. Neočekávané chování těchto zařízení představuje pro univerzitu závažný provozní problém pro následující roky.

Zmíněné zařízení, domácí router značky Netgear, používal SNTP protokol pro získávání reálného času, neboť sám neměl ani hodiny, ani baterii. IP adresa univerzitního časového serveru byla v routeru natvrdo nastavena výrobcem. Čím víc kusů bylo prodáno, tím víc rostl souhrnný provoz na univerzitní časový server.

Šlo o opravitelný problém? Mohl výrobce svolat všechna zařízení k opravě?

Jak Netgear, tak i ostatní členové přezkumného týmu považují za nepravděpodobné, že by víc než několik málo majitelů dotčeného zařízení toto zařízení vrátilo, neboť se zdá být plně funkční. Také jen velmi malé množství zákazníků výrobek zaregistrovalo u výrobce, takže by bylo velmi obtížné všechny zákazníky kontaktovat.

Jde o věc, co leží někde ve skříni. Pozornost je jí věnována jen, když přestane fungovat. Neexistuje způsob, jak v zařízení na dálku vyměnit software a neexistuje ani způsob, jak upozornit uživatele, že je nutné zařízení aktualizovat. Takže problém přetrvává.

V tomto případě šlo o jedinou oběť hlouposti věcí připojených k internetu. Jiné formy nebezpečné hlouposti ovlivňují mnohem více lidí. Webová stránka openresolverproject.org popisuje snahy Jareda Maucha dokumentovat míru rozšíření internetových zařízení, které jsou fakticky tzv. otevřenými DNS resolvery.

Jde opět obvykle o domácí modemy, propojující domácí síť k internetu. Původní záměr byl pravděpodobně poskytnout odpovědi na DNS dotazy z vnitřní sítě přeposíláním dotazů do vnější sítě. Zdá se ale, že v některých případech se softwaru nepodařilo správně rozlišit, která síť je vnitřní a která vnější, takže odpovídal stejným způsobem i na dotazy z vnější sítě. Takto hloupých zařízení je velmi mnoho; podle zmíněné webové stránky jich bylo v říjnu 2013 28 milionů.

Takováto zařízení mohou být zapojena do nebezpečného chování tak, že jsou jim poslány DNS dotazy z internetu. Otevřený resolver se pokusí dotaz odpovědět (protože nedokáže rozlišit mezi vnitřní a vnější sítí) a pošle odpověď na zdrojovou adresu dotazu. DNS odpověď může být výrazně větší než dotaz. Jedna forma nebezpečného útoku pak tedy spočívá v hromadném oslovení takovýchto otevřených resolverů s dotazem na jediný autoritativní DNS server s cílem takový server zahltit. Jiná forma útoku používá falšované zdrojové IP adresy, konkrétně adresy zamýšlené oběti útoku, a použije miliony otevřených resolverů k posílání DNS odpovědí směrem k oběti.

Stejně jako u problému se SNTP v Netgearu, ani zde není možnost snadné nápravy. Jde o nespravovaná zařízení ležící kdesi ve skříni. Nikdo se o ně nestará.

I u nás v APNIC jsme narazili na podobný problém. Jeden z našich serverů byl soustavně zatížen zhruba 5000 dotazy za sekundu, všechny na jediný skript, vracející IP adresu, odkud dotaz přišel. Zkrátka šlo o jednoduchou variantu služby „Jaká je moje IP adresa“. Když jsme chtěli zmíněné servery přestěhovat, začalo nás zajímat, odkud se takový provoz bere. Zdá se, že stejně jako předchozí zkušenosti se špatným řízením kvality softwaru domácích routerů, i tady nějaký výrobce vložil URL skriptu pro zjištění IP adresy na stránkách APNIC do softwaru nějakého digitálního videorekordéru nebo televizoru. A software daného zařízení z nějakého důvodu danou adresu pravidelně dotazuje.

Zdá se, že APNIC je zde jediná oběť. Problém je nicméně o něco hlubší. APNIC teď může určitě podle dotazů zjistit, kolik zařízení bylo prodáno a kam byly instalovány. Kromě úniku informací o trhu zde vyvstává ještě několik nepříjemných otázek. Co když APNIC změní skript tak, že bude vracet špatnou IP adresu? Co když skript zasekne TCP spojení a nikdy neodpoví? Došlo by k pádu zařízení, kdyby skript vrátil namísto několika očekávaných bajtů několika megabajtovou odpověď? Mohla by odpověď přetéct přes velikost přijímacího bufferu a přepsat zásobník operačního systému zařízení? Dalo by se odpovědí skriptu dostat dovnitř zařízení a proměnit ho v dálkově ovládaného robota? Je jasné, že my nebudeme nic z toho zkoušet. Není to ale příjemná pozice, vědět, že stojíme na kritické cestě někoho jiného.

Možná se to celé zlepší, až software v podobných zařízeních začne být spravovaný. Dvanáct procent všech chytrých telefonů prodaných minulý rok, zhruba 180 milionů kusů, je poháněno softwarem iOS od Apple. Součástí obchodního modelu Apple je App Store, a aby jej lidé využívali, jsou telefony zamčené. Každý vyhledávač vás ale s radostí nasměruje k instrukcím, jak svůj iPhone odemknout, k čemuž se zneužívají chyby ve většině verzí iOS. Takže nejde jen o problémy se softwarem v levných nespravovaných zařízeních, jde i o masově vyráběná drahá zařízení, kde je silný komerční zájem na nejlepší kvalitě softwaru.

Když uvažujeme o internetu věcí, máme na mysli svět meteostanic, webkamer, chytrých aut, osobních tréninkových monitorů a tak dále. Máme ale sklon zapomínat na to, že všechna tato zařízení jsou postavena z vrstev softwaru jiných lidí do produktu s nejnižší možnou cenovou hladinou. Je vcelku znepokojující uvědomit si, že webkamera, kterou jste právě nainstalovali, je postavena na bezpečnostním modelu, který se dá shrnout do jediného slova: ano. Tato kamera vystavuje pohled na váš dům pro celý internet. Ještě více znepokojující může být fakt, že vaše elektronická peněženka je uložena v zařízení, které je postaveno na ohromné složenině open source softwaru z velké části nejasného původu, s bezpečnostním modelem, který nikdo pořádně nechápe, ale o kterém se snažíte být přesvědčeni, že funguje.

Co s tím budeme dělat?

Bylo by pěkné, myslet si, že jsme přestali dělat chyby v kódu a od teď už bude software ve všech našich věcech perfektní. To je však beznadějně idealistická představa. Tak to nebude. Software nebude dokonalý. Vždy v něm budou zranitelnosti.

Bylo by pěkné, myslet si, že internet věcí se vytvaruje do podoby trhu, kde záleží na kvalitě a kde zákazníci budou volit dražší produkt, i když je funkčně srovnatelný s levnějším, který však nebyl podrobně testován na bezpečnostní chyby. Ale to je také beznadějně idealistická představa.

Internet věcí bude nadále trhem s kompromisy mezi kvalitou a cenou. Bojím se, že obětujeme kvalitu ve prospěch nízké ceny. Pokud to tak bude, co nás zastaví od dalšího zamořování internetu ohromným množstvím rozmanitých nespravovaných programovatelných zařízení s vestavěnými zranitelnostmi?

Co můžeme udělat k tomu, aby svět těchto levných zařízení byl méně hloupý a méně škodlivý?

Jasné odpovědi na tuto otázku se nacházejí těžko. Nicméně si myslím, že už časti odpovědi známe.

Jedna část odpovědi je, že bychom opravdu měli používat IPv6 pro internet věcí. S IPv4 je spojena spousta problémů, jeden z nich je příliš malý adresní prostor. Pokud chcete mít svou věc vystavenou na internetu, každý tu vaši věc najde také.

ZMap je open-source síťový skener, který umožňuje výzkumníkům snadno uskutečnit výzkum na celém internetu. S jedním strojem a dostatečně dimenzovaným síťovým připojením umožnuje ZMap provést kompletní sken IPv4 adresního prostoru za méně než 5 minut, čímž se přibližuje k teoretické kapacitě desetigigabitového ethernetu.

Ano, jen 5 minut stačí k proskenování celého IPv4 adresního prostoru. Na IPv4 se není kam schovat. Pokud chcete danou věc vidět, ostatní ji uvidí také.

Jak je to s IPv6? Pokud trvá 5 minut skenování 4 miliard adres, jak dlouho by trvalo skenování všech 340 sextilionů (1036) adres? Při stejné rychlosti by to trvalo 70 triliard (1021) let. I při obvyklém omezení na používané adresy a další heuristice není skenování IPv6 prostoru realisticky proveditelné. Už jen průzkum 64bitového identifikátoru rozhraní zabere stejným tempem 41 tisíc let, takže IPv6 systém, který řádně používá náhodné privátní adresy se sám o sobě skrývá v ohromném prostoru k prohledání.

Jiná část odpovědi je vyhnout se zbytným vnějším závislostem. Například se nesnažit vyzradit svou existenci voláním externích skriptů. Nespoléhat na cizí internetové zdroje, že budou online a funkční.

A nakonec je zajisté užitečné být paranoidní. Nevěřte internetu. To není to samé jako „nepoužívejte internet“. Využívejte připojení a jeho služby, ale nebuďte přehnaně důvěřiví. Pokud používáte DNS k mapování jmen na IP adresy, používejte interní DNS knihovnu, která validuje všechno co přijme namísto spoléhání na to, že externí resolver by vám přece nelhal. Pokud používáte zabezpečené kanály pro přístup k zařízení, což byste měli, použijte bezpečnostní model, který bude úzce zaměřený na použití konkrétního pevného bodu důvěry. Představte si internet jako nepřátelské území: jak můžete zabránit vašemu zařízení, aby bylo zajato a zneužito ostatními?

zabbix_tip

Nic není dokonalé. Software se vyvíjí rychleji než hardware. Dobře si rozmyslete, jak chcete spravovat zařízení která jsou už nasazena v terénu na internetu. Jak můžete taková zařízení aktualizovat, abyste opravili katastrofální chybu?

Žádná z těchto pouček není nová. Kdybychom je byli schopni aplikovat retrospektivně, už bychom se dnes nedívali na 30 milionů nespravovaných otevřených DNS resolverů uvažujíce co s nimi budeme dělat. Tady už je ale dokonáno. Mnohem důležitější je vyhnout se dalšímu rozšiřování skupiny takto škodlivých věcí. Pokud míříme k internetu stovky miliard věcí v nastávajících pěti letech, pak se ujistěme, že si pamatujeme předchozí chyby a jsme schopni se z nich poučit. Opravdu se musíme ujistit, že nepůjde o internet sta miliard hloupých věcí, které budou připraveny ke konání zla.

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 »