DNSSEC snadno a rychle s automatickou správou v serveru BIND

6. 5. 2025
Doba čtení: 7 minut

Sdílet

Autor: Depositphotos
Po dlouhá léta podepisujeme DNS zóny pomocí vestavěné funkce nejznámějšího DNS serveru BIND 9. Aktuální verze přinášejí novější plně automatizovaný přístup, který původní postup nahrazuje.

Předchozí stav

Co se dozvíte v článku
  1. Předchozí stav
  2. Automatický DNSSEC
  3. Co se událo
  4. Bezpečná delegace z nadřazené zóny
  5. Použitá politika
  6. Přechod z původního řešení pro BIND 9.9

Před lety vyšel na Rootu článek DNSSEC s BIND 9.9 snadno a rychle, ve kterém jsme popisovali nový způsob podepisování DNS zóny pomocí takzvaného inline signingu, který umožňuje zapnout podepisování a přitom zachovat původní zónové soubory beze změn.

Původní způsob byl poloautomatický a umožňoval sice generovat a udržovat podpisy, ale správa klíčů byla ponechána na uživateli. Bylo tedy potřeba provést několik kroků, než bylo možné zapnout vytvoření podepsané kopie zóny pomocí volby  auto-dnssec maintain.

Tato volba je už nějakou dobu označena za zastaralou, protože obnovuje jen podpisy, zatímco klíče je třeba měnit ručně. BIND přitom už nějakou dobu umí plně automatickou správu DNSSEC, včetně klíčů, podobně jako třeba Knot DNS.

Pozor na to, že počínaje verzí 9.20 je původní způsob konfigurace nefunkční a pokud jste před lety konfigurovali svůj BIND původním způsobem s volbou auto-dnssec, po aktualizaci už se nerozeběhne. Dříve či později tedy budeme muset provést úpravy popsané v tomto článku. Je lepší to udělat hned, protože verze 9.16 a 9.18 podporují oba přístupy, takže se změnou připravíte na budoucnost.

Automatický DNSSEC

Nová metoda konfigurace se nazývá Key and Signing Policy (KASP) a popisuje, jak se mají spravovat klíče a jak má probíhat podepisování zóny. Je to vymyšleno tak, že pro většinu běžných situací by měla být vhodná výchozí politika default, která nabízí aktuální doporučované postupy pro práci s DNSSEC. Později si ukážeme, jak politika vypadá uvnitř a jak si můžeme vytvořit vlastní.

Prvním krokem po instalaci je vytvoření zónového souboru. V souboru /etc/bind/named.conf.options je nastavená volba directory, která ukazuje na pracovní adresář. Pokud v konfiguraci uvedeme relativní jméno souboru, bude soubor hledán právě v tomto adresáři. Volba je po instalaci nastavena na /var/cache/bind, což je pro BIND zapisovatelný adresář.

# ls -ld /var/cache/bind
drwxrwxr-x 2 root bind 5 Apr 24 11:35 /var/cache/bind

To je okolnost velmi důležitá, protože BIND si sem později bude zapisovat různé soubory, včetně kopie naší zóny doplněné o záznamy s podpisy. Do adresáře tedy musí mít právo zápisu, jinak celý proces selže. Není ale rozumné přímo v tomto adresáři udržovat ručně editované soubory, protože o ně můžeme přijít.

Proto si v /etc/  vytvoříme adresář zones, kde budeme pracovat. Z /var/cache/bind/  si pak uděláme symbolický odkaz. Hned si také vytvoříme první zónový soubor a zapíšeme do něj záznamy:

# mkdir /etc/bind/zones/
# vim /etc/bind/zones/example.com.db
$ORIGIN example.com.
$TTL 60
@ IN SOA example.com. hostmaster (
    1 ; serial
    120 ; refresh (2 minutes)
    10 ; retry (10 seconds)
    3600 ; expire (1 hour)
    60 ; minimum (1 minute)
    )

@   NS  ns.example.com.
@   A   192.0.2.1
@   AAAA    2001:0db8::1

Obsah tohoto souboru zůstane v naší správě a BIND do něj nebude zasahovat. To je hlavní výhoda inline signingu, kdy se nám automaticky spravované podpisy nemíchají do lidsky čitelného a jednoduchého zónového souboru.

Nezapomeneme soubor nalinkovat do pracovního adresáře:

# ln -s /etc/bind/zones/example.com.db /var/cache/bind/example.com.db

Nyní si přidáme novou zónu do konfigurace autoritativního DNS serveru. Připíšeme ji na konec souboru  /etc/bind/named.conf.local.

# vim /etc/bind/named.conf.local

zone "example.com" {
      type primary;
      file "example.com.db";
      dnssec-policy default;
      inline-signing yes;
  };

Volby říkají, že jde o primární autoritativní zónu (master), nachází se v souboru daného jména, chceme aplikovat výchozí politiku a chceme strojově pracovat s kopií zóny. Nyní požádáme server, aby si znovu načetl upravený konfigurační soubor:

# rndc reload

Co se událo

Po restartu se můžete podívat do pracovního adresáře /var/cache/bind/, ve kterém přibyla řada souborů, jejichž názvy vycházejí z původního názvu našeho zónového souboru. Je tu soukromý klíč (přípona .private), stavové informace k tomuto klíči ( .state), žurnál ( .jnl) a především soubor s příponou .signed, který obsahuje kopii zóny včetně automaticky doplněných podpisů.

Do tohoto souboru se můžeme podívat, ale budeme potřebovat utilitu named-compilezone, která nám surová data interpretuje a vypíše v čitelné podobě.

# named-compilezone -f raw -j -o - example.com /var/cache/bind/example.com.db.signed
zone example.com/IN: loaded serial 4 (DNSSEC signed)
example.com.              60 IN SOA     example.com. hostmaster.example.com. 4 120 10 3600 60
example.com.              60 IN RRSIG   SOA 13 3 60 20240508094858 20240424084858 48071 example.com. PulAb4FsFygUajpDPJqiEAcT93sUOFYS5LToErfqsm/uF3yafqP6sPsX YtZkei4I01OrZJc0UVpTTCZQWMPVEg==
; resign=20240508094858
example.com.              60 IN NS      ns.example.com.

…(zkráceno)

Na aktuální stav podepisování se můžeme kdykoliv zeptat ovládací utilitou  rndc:

# rndc dnssec -status example.com.
dnssec-policy: default
current time:  Wed Apr 24 12:00:52 2024

key: 48071 (ECDSAP256SHA256), CSK
  published:      yes - since Wed Apr 24 11:48:58 2024
  key signing:    yes - since Wed Apr 24 11:48:58 2024
  zone signing:   yes - since Wed Apr 24 11:48:58 2024

  No rollover scheduled
  - goal:           omnipresent
  - dnskey:         rumoured
  - ds:             hidden
  - zone rrsig:     rumoured
  - key rrsig:      rumoured

Bezpečná delegace z nadřazené zóny

Nakonec je potřeba přidat DS záznam s otiskem veřejného klíče do nadřazené zóny. BIND sám generuje záznamy typu CDS/CDNSKEY, které dovolují celý proces automatizovat. Ty se v zóně objeví až po určité době, kdy je jistota, že všechny kešující resolvery mají aktuální verzi zóny.

V případě, že nadřazená zóna automatickou správu podporuje (například jako .CZ), není potřeba u registrátora domény nic měnit a registr se informaci o podepsání zóny dozví sám a po sedmi dnech přidá otisky klíčů v záznamu typu DS do nadřazené zóny.

V případě, že automatizovaný postup provozovatel nadřazené zóny nepodporuje, můžeme příslušný otisk získat přímo na serveru pomocí utility  dnssec-dsfromkey.

# cd /var/cache/bind/
# dnssec-dsfromkey -2 Kexample.com.+013+27231

example.com. IN DS 27231 13 2 B4012D35909E7757A622EC72D63365C4F09DD25EEE50C01DF870F7F3DF283962

Použitá politika

BIND se při své práci s podpisy a klíči řídí takzvanou politikou. To je seznam konfiguračních pravidel, která se vztahují na různé vlastnosti a parametry důležité pro DNSSEC. My nyní používáme výchozí politiku (default), takže ji nemusíme definovat. Můžeme se ale podívat na to, jak vypadá.

Není součástí běžných konfiguračních souborů, ale najdeme ji ve zdrojových kódech serveru BIND. Autoři mají v plánu ji upravovat podle aktuálních bezpečnostních trendů.

dnssec-policy "default" {
    // Klíče
    offline-ksk no;
    keys {
        csk key-directory lifetime unlimited algorithm 13;
    };

    // Časování klíčů
    cdnskey yes;
    cds-digest-types { 2; };
    dnskey-ttl 3600;
    publish-safety 1h;
    retire-safety 1h;
    purge-keys P90D;

    // Časování podpisů
    signatures-jitter 12h;
    signatures-refresh 5d;
    signatures-validity 14d;
    signatures-validity-dnskey 14d;

    // Parametry zóny
    inline-signing yes;
    max-zone-ttl 86400;
    zone-propagation-delay 300;

    // Parametry rodičovské zóny
    parent-ds-ttl 86400;
    parent-propagation-delay 1h;
};

Výchozí politika používá jediný klíč typu ECDSA, který má neomezenou životnost. Většina parametrů má rozumné hodnoty, ale je potřeba dát pozor na způsob podepisování negativních odpovědí pomocí záznamu typu NSEC. Ten umožňuje klientovi projít postupně všechny položky v zónovém souboru, i při vypnuté podpoře přenosu zón.

Je-li to pro nás problém, můžeme přejít na záznamy typu NSEC3. Lze proto napsat vlastní politiku, která nechá většinu voleb v původním stavu a upraví jen to nejnutnější. Do konfiguračního souboru pak připíšeme něco jako:

dnssec-policy "nsec3" {
    nsec3param iterations 1 optout false salt-length 16;
};

Všechny parametry za klíčovým slovem nsec3param jsou nepovinné. Pokud je vynecháme, použijí se výchozí hodnoty.

Přechod z původního řešení pro BIND 9.9

Pokud jste před lety nastavili BIND podle našeho článku, bude přechod na nové řešení celkem snadný. Původní řešení použilo taktéž jediný klíč typu ECDSA, stejně jako to předepisuje výchozí politika. Pokud je při aktivaci nového řešení zóna podepsána kompatibilním klíčem, bude tento klíč používán i nadále.

Pokud však používáte jinou konfiguraci klíčů (například RSA nebo dva samostatné klíče KSK/ZSK), je třeba upravit výchozí politiku tak, aby odpovídala tomuto stavu. V opačném případě BIND nekompatibilní klíče zahodí a nahradí jinými. To by způsobilo rozpad řetězce důvěry a tedy nefunkčnost zóny.

Stačí tedy upravit konfigurační soubor, konkrétně dvě volby: cestu k adresáři s klíči a pak samotný způsob správy DNSSEC. Klíče byly původně uloženy v /etc/bind/keys/, kam ale BIND nemůže zapisovat. My tedy klíče přesuneme do pracovního adresáře /var/cache/bind/ a změníme jim vlastníka a skupinu na  bind.

# mv /etc/bind/keys/Kexample.com* /var/cache/bind/
# chown bind: /var/cache/bind/Kexample.com*

Dále upravíme zmíněné volby v konfiguraci:

zone "example.com" {
        type master;
        file "example.com";
        inline-signing yes;
        auto-dnssec maintain;
        key-directory "/etc/bind/keys";
        dnssec-policy default;
        key-directory "/var/cache/bind";
};

Po provedení změn řekneme DNS serveru, aby načetl znovu konfiguraci:

# rndc reload

V logu pro jistotu zkontrolujme, že si server na nic nestěžuje.

# journalctl -eu named

Můžeme se také podívat na stav podepisování a tím zároveň ověřit, že jsme u dané zóny přešli na nový způsob správy.

prace_s_linuxem_tip

# rndc dnssec -status example.com

Po určité době, dané časovacími parametry politiky, začne BIND vystavovat CDS a CDNSKEY záznamy signalizující nadřazené zóně, že aktuálně používaný klíč má být použit pro vytvoření DS záznamu. Protože tento klíč už v nadřazené zóně je, nedojde k žádné změně. Nicméně v případě, že nadřazenou zónou je doména CZ, způsobí přítomnost CDNSKEY záznamu přechod na automatickou správu keysetů. Registr tedy od domény odpojí původní manuálně konfigurovaný keyset a připojí k doméně nový, automaticky generovaný, se stejným obsahem.

Tím je migrace hotová bez výpadku, protože jsme nezměnili algoritmus a počet klíčů. V každém případě je vhodné celý postup ověřit na testovací zóně a při migraci ostré zóny být připraven odstranit DS záznamy z nadřazené zóny, pokud by se objevily nečekané komplikace.

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

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 »