Správa DNS zónových souborů v Gitu

20. 12. 2018
Doba čtení: 8 minut

Sdílet

Ilustrační obrázek
Autor: Depositphotos – stori
Ilustrační obrázek
Zónové soubory jsou stále tím nejjednodušším způsobem správy DNS dat na autoritativních serverech. V článku si popíšeme, jak je možné použít Git pro správu historie zónových souborů a ochranu před chybami.

Tradiční zónové soubory jsou stále populární, zejména v mezi poskytovateli přístupu k internetu, kde správa DNS není hlavním předmětem činnosti – řeší se většinou jen reverzní záznamy – a požadavek na rychlé změny není velký. Tím nejjednodušším řešením je editace zónového souboru přímo na primárním DNS serveru a použití klasických mechanizmů zónového přenosu (AXFR, případně inkrementální IXFR) pro přenos na sekundární autoritativní servery. V takovém případě je třeba zónový soubor editovat obezřetně; vložení syntaktické chyby má za následek výpadek služby, nezvýšení sériového čísla zóny při úpravě způsobí, že se zóna nepřenese a každý autoritativní server tedy bude nabízet jiná data.

Použití skrytého primárního serveru situaci vylepšuje. Skrytý primární server drží zónové soubory, ale není zveřejněn jako autoritativní server pro danou doménu. Jeho jediným účelem je přenést zónu zónovým přenosem na veřejné sekundární servery. Díky tomu je zaručeno, že všechny veřejné DNS servery budou mít stejná data a že případný výpadek, způsobený například chybou v zónovém souboru, se neprojeví veřejně. Při podepisování zóny pomocí DNSSEC je navíc praktické nemít privátní klíče přímo na veřejném DNS serveru.

Často je praktické udržovat nějakou míru historie změn v zónových souborech. Tradiční řešení s komentáři přímo uvnitř souborů úplně dobře neškáluje a neobstojí v konkurenci moderních systémů pro správu verzí, jako je například Git. Na druhou stranu, ukládání každé změny do gitovského repozitáře přidává administrátorům další práci. I když jde o sérii jednoduchých úkonů, jejich manuální provádění je nepohodlné a náchylné k chybám. Naštěstí Git obsahuje podporu pro hooks, tedy háčky, kterými lze upravit průběh jednotlivých kroků a tak většinu práce automatizovat. Systém může nakonec vypadat třeba takto:

Většinu komponent tvoří běžně dostupný open-source software od několika výrobců. Jedinou výjimkou je software pro integraci Git repozitáře s DNS serverem. Objevil jsem pouze jeden projekt jménem GitZone, který v sobě kombinuje správu gitovského repozitáře a napojení na DNS server v jednom skriptu v Perlu. Takový přístup mi nevyhovoval – chtěl jsem něco lehčího, co bude možné používat s existujícími a prověřenými správci Git repozitářů, jakým je třeba Gitolite. Po určitém výzkumu jsem se rozhodl napsat vlastní univerzální řešení.

Představujeme dzonegit

Projekt dzonegit je sada háčků pro Git pro automatizaci správy zónových souborů. Je napsán v Pythonu 3.5 a závisí pouze na programu named-compilezone, který je součástí DNS serveru BIND. V háčku pre-commit se pokusí zkompilovat všechny změněné zónové soubory. Pokud se kompilace nezdáří, je commit odmítnut. V opačném případě je porovnáno sériové číslo změněných zón se sériovým číslem předchozích verzí. Pokud se nezvýšilo, je commit rovněž odmítnut, protože by se nová verze zóny nepřenesla.

Vzhledem k tomu, že všechny háčky jsou lokální k danému Git repozitáři, nelze jejich spuštění vynutit na vzdáleném repozitáři. Z toho důvodu se stejné kontroly validity dat provádějí i na straně DNS serveru během přijímání revize od uživatele. K tomu slouží háček pre-receive nebo update  – oba dělají totéž, záleží jen na tom, který se jednodušeji integruje se správcem repozitáře. Díky tomu je zajištěno, že v repozitáři na DNS serveru jsou pouze validní zónové soubory, jejichž sériové číslo se s každou změnou zvyšuje.

Poslední háček, který dzonegit  poskytuje, je post-receive. Je spouštěn po každé aktualizaci repozitáře na serveru. Jeho úkolem je vystavit obsah repozitáře v externí pracovní kopii, odkud budou zónové soubory načteny DNS serverem. Dále je vygenerován úryvek konfiguračního souboru pro DNS server, obsahující definice všech zón v repozitáři. Například pro BIND je pro každou zónu potřeba následující řádek v konfiguraci:

zone "example.com" { type master; file "/path/to/example.com.zone"; }; 

Aby bylo řešení univerzální, tyto úryvky jsou generovány z jednoduché JSON šablony. Pro výše uvedený příklad může šablona vypadat takto:

{
  "header": "# Autogenerated by dzonegit on $datetime. Do not edit.\n",
  "item": "zone \"$zonename\" { type master; file \"$zonefile\"; };"
} 

Posledním krokem, který háček post-receive provádí, je notifikace DNS serveru, že došlo ke změně zónových souborů. Slouží k tomu dva příkazy. Příkaz uložený v proměnné zonereloadcmd je volán pro každou zónu, jejíž obsah se změnil; název zóny je přidán na konec příkazu. Druhý příkaz v proměnné reconfigcmd je volán pokaždé, je-li nějaký zónový soubor přidán nebo odebrán.

Všechny proměnné nástroje dzonegit jsou uloženy jako konfigurační volby Git repozitáře. Běžným příkazem git config je možné nastavit je jak pro repozitář, tak i pro uživatele nebo globálně.

Podepisování technologií DNSSEC

V počátcích technologie DNSSEC byl jediným dostupným řešením nástroj dnssec-signzone. Ten načte zónový soubor, podepíše ho a zapíše vedle nový, podepsaný soubor. Tuto utilitu je teoreticky možné stále používat ještě před vložením zónového souboru do repozitáře, takové použití by ale bylo nepohodlné a vyžadovalo by pravidelnou údržbu podpisů, jejichž platnost je časově omezená. Dnes je situace jiná, na trhu je mnoho dobrých open-source řešení nabízejících automatickou správu podpisů i klíčů – například BIND, OpenDNSSEC, PowerDNS, nebo český Knot DNS.

Aby byl systém co nejvíce modulární, je dobré oddělit proces DNSSEC podpisů a správy klíčů do samostatného funkčního bloku. V režimu zvaném bump-in-the-wire se nepodepsané zóny přenášejí na podepisovač, který následně přenáší podepsané zóny na veřejné sekundární servery.

Jako podepisovač jsem zvolil Knot DNS. Argumenty pro jeho výběr jsou v podstatě shodné s argumenty prezentovanými organizací RIPE NCC. Během testování jsem objevil několik chyb v implementaci správy klíčů. Všechny byly vývojáři promptně opraveny, opravy jsou ale dostupné jen v nejnovějších vydáních. Z toho důvodu je třeba silně doporučit použití oficiálních balíčků namísto distribučních, které mohou být zastaralé.

Nastavení Knot DNS je velmi jednoduché. V hlavním konfiguračním souboru nastavíme šablonu pro zóny a politiku podpisů:

template:
  - id: default
    storage: "/var/lib/knot"
    zonefile-load: none   # vůbec nepoužíváme zónové soubory
    zonefile-sync: -1
    journal-content: all  # obsah zón držíme v žurnále
    master: primary       # odkaz na primární server
    acl: acl_primary      # povol zprávy NOTIFY z primárního serveru
    acl: acl_secondary    # povol zónové přenosy na sekundární servery
    notify: secondary     # posílej sekundárním serverům zprávy NOTIFY
    dnssec-signing: on
    dnssec-policy: ecdsa

policy:
  - id: ecdsa
    algorithm: ecdsap256sha256
    zsk-lifetime: 30d
    rrsig-lifetime: 30d
    rrsig-refresh: 15d
    nsec3: on 

Nakonec do souboru vložíme úryvek vygenerovaný post-receive háčkem nástroje dzonegit  – je možné vygenerovat více různých úryvků zároveň. V tomto případě můžeme využít proměnných pro každou zónu a tak nastavit různé politiky podepisování či nepodepisování pro různé zóny.

Sdílet či nesdílet

V prostředí web hostingu, přinejmenším v Česku, je velmi oblíbené použití jedné sady DNSSEC klíčů pro všechny hostované zóny. Při velkém množství hostovaných zón se tak dramaticky snižuje provozní náročnost podepisování. Navíc FRED, open-source registr vyvíjený a provozovaný sdružením CZ.NIC, takové sdílení klíčů mezi doménami ulehčuje použitím takzvaných Keysetů. Tyto objekty obsahují veřejné DNSSEC klíče a mohou být přiřazeny k libovolnému počtu domén. Je to poměrně velký rozdíl proti běžně používanému řešení podle RFC 5910, kde se DS záznam přidává přímo jako atribut dané domény. Vzhledem k tomu, že DS záznam je otiskem veřejného klíče a doménového jména, není možné jej mezi doménami sdílet.

Minulý rok však CZ.NIC jako první na světě zprovoznil automatickou správu keysetů, takže bezpečnou delegaci z .cz  domény je možné vytvořit a udržovat aktuální čistě pomocí signalizace uvnitř DNS, definované v RFC 7344. Tím se provozní náklady na udržování bezpečné delegace snížily na nulu, bez ohledu na sdílení či nesdílení klíčů.

Z pohledu správy klíčů je nesdílení klíčů mezi zónami jednodušší a více otestované. Dokud nespravujete tisíce zón, asi je lepší se sdílení klíčů vyhnout.

Automatizujte, co se dá!

Automatická správa bezpečné delegace výrazně zjednodušuje správu DNSSECu. Po správném úvodním nastavení není třeba žádná další pravidelná manuální akce. Bohužel existuje jen několik málo nadřazených zón, které automatickou správu podporují, v současné době to jsou .cz, .cr, .ch a .li. Pro všechny ostatní nadřazené zóny je potřeba stále provádět pravidelně výměny klíče ručně.

Nejjednodušší je samozřejmě ignorovat problém. Při použití moderních DNSSEC algoritmů ECDSA s klíčem délky 256 bitů jde o klíče tak silné, že je není potřeba měnit minimálně několik let.

Jiné řešení spočívá v použití CDS/CDNSKEY záznamů, který produkuje podepisovací software, jako univerzální signalizaci nutnosti výměny klíče a implementovat komunikaci s nadřazenou zónou ve vlastní režii. Zabezpečení reverzních zón, delegovaných z RIPE NCC, se ukazuje jako nejjednodušší varianta, protože je k dispozici jednoduché a dobře dokumentované REST API, kterým je možné číst a editovat obsah RIPE databáze, kde jsou uloženy i data bezpečných delegací.

Vyrobil jsem demonstrační příklad porovnávající DS záznam uložený v RIPE databázi s otiskem získaným z CDS  záznamu v zóně, validovaném pomocí DNSSEC. Pokud je nalezen nový otisk, je databáze aktualizována. Během RIPE 77 toto řešení vzbudilo v komunitě jistý zájem, je tedy možné, že v budoucnu bude podobné řešení nasazené přímo na straně RIPE NCC. Obdobný přístup je možné použít i s jinými registry, tedy za předpokladu, že registrátor podporuje DNSSEC i na jiných než .cz a .eu doménách a navíc má pro změnu příslušné API.

Největší výzva ovšem spočívá v udržování bezpečných delegací ze zón hostovaných na naší vlastní platformě. Je k tomu potřeba buď nějaký systém automatických commitů do Git repozitáře, kdykoli je potřeba změnit obsah DS záznamu, nebo nějaký skrytý mezikrok přidávání DS záznamů po načtení z repozitáře, před předáním DNS serveru. K tomu by se dal použít například smudge filtr v Gitu.

Shrnutí

Na popsaný systém správy zónových souborů jsme převedli všechny domény hostované ve sdružení CESNET počátkem září 2018, po několikaměsíčním testování. Přechod na Knot DNS nám umožnil přejít ze sdíleného RSA klíče na samostatné ECDSA klíče a zavést DNSSEC i na domény, u kterých nejsme držitelem a nemáme tedy přístup k administraci dané domény. Snížení pracovní náročnosti a bezpečnostní funkce v podobě okamžitých kontrol udělalo rutinní činnost editace DNS záznamů mnohem jednodušší pro operátory. Všechny části systému jsou open-source, příspěvky komunity jsou vítány.

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 »