SPF: proti spamu i přeposílání pošty

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

Sdílet

Ilustrační obrázek
Autor: Depositphotos – stori
Ilustrační obrázek
Sender Policy Framework je jedno z opatření, jak zvýšit důvěryhodnost e-mailové komunikace pomocí speciálních autorizačních DNS záznamů. Za určitých okolností ale může SPF způsobit nefunkčnost přeposílané pošty. V prvním díle nového seriálu o e-mailových reputačních systémech se podíváme, jak SPF funguje.

Principem SPF (původně zkratka znamenala Sender Permitted From) je dát možnost vlastníkovi DNS domény deklarovat, které servery jsou oprávněny odesílat e-mailové zprávy jménem domény. Příjemce pošty pak může validovat, zda poštu od dané domény dostává z autorizovaného serveru a podle toho zohlednit další nakládání se zprávou.

Majitel domény example.com může například specifikovat politiku, že poštu z adres @example.com mohou předávat pouze servery z adresního rozsahu 192.0.2.0/24 takto:

example.com   IN  TXT  "v=spf1 ip4:192.0.2.0/24 -all" 

Pass, neutral, fail, softfail

Obsah TXT záznamu obsahuje vždy povinné záhlaví v=spf1 následované mezerou oddělenými slovy, která definují povolené, nebo naopak zakázané rozsahy adres. Každé slovo může být uvozeno kvalifikátorem, jejichž význam shrnuje následující tabulka:

znak název význam
+ pass daný rozsah adres vyhovouje politice (výchozí)
? neutral pro daný rozsah adres politika neexistuje
- fail daný rozsah adres nevyhovouje politice
~ softfail daný rozsah adres spíše nevyhovouje politice

Jako rozsah adres pak mohou být kromě výše uvedeného příkladu se slovy all pro všechny adresy a ip4  pro daný rozsah adres také klíčová slova a a mx, která doplní IP adresu získanou DNS dotazem na příslušný typ doménového záznamu. Nejlepší současná praxe ale doporučuje se takovýmto zřetězením vyhnout za účelem snížení počtu DNS dotazů, které musí validátor vykonat.

Pravidla se při validaci vyhodnocují v pořadí, v jakém jsou zapsána, po nalezení první shody se další nezkoumají. Neexistující politika vrací stav neutral, je tedy ekvivalentní existenci záznamu

example.com   IN  TXT  "v=spf1 ?all" 

Příjemce pošty, který SPF záznam validuje, tak činí ideálně ještě v průběhu SMTP komunikace. Nejprve by měl pomocí SPF validovat jméno hostitele, kterým se představí klient v příkazu HELO/EHLO, následně validuje adresu domény, uvedenou v příkazu MAIL FROM. Pokud na základě vyhodnocení SPF rozhodnuto o nepřijetí zprávy, měla by být odmítnuta již během SMTP komunikace.

TXT nebo SPF?

SPF je jedním z protokolů, jehož vývoj probíhal ve spěchu a tak trochu mimo půdu IETF. Jedním z důsledků kvapného zavádění je způsob, jakým protokol SPF přikládá speciální význam TXT záznamům v DNS. Ty byly původně určeny pro nestrukturované textové informace. SPF těmto informacím přikládá speciální význam a dopouští se tak klasické chyby známé ve světě databází jako metadata v datech.

Na půdě IETF proto kdysi vznikla snaha situaci napravit zavedením nového typu DNS záznamu SPF, který bude mít stejnou sémantiku jako TXT záznam, bude však určen jen pro potřebu SPF. Tato snaha ovšem dopadla fiaskem; než se podpora pro nový typ DNS záznamu dostala do DNS serverů a jejich uživatelských rozhraní, bylo SPF již velmi rozvinuto. Validátory tak musely kontrolovat dva různé DNS záznamy a řešit případně rozpory. Přítrž tomu učinil nejnovější standard RFC 7208, který záznam typu SPF rezervuje pro budoucí revize standardu, zatímco pro aktuální verzi 1 nařizuje – v souladu se současnou praxí – použití záznamu typu  TXT.

Problém s přeposíláním pošty

Mnohem zásadnější je ale koncepční problém s přeposíláním pošty na straně příjemce. Mějme příklad se třemi entitami:

  • A publikuje SPF politiku, obsahující tvrdé selhání pro nepovolené adresy
  • B nemá s SPF nic společného, jen provozuje e-mailové schránky a nabízí svým uživatelům přesměrování pošty na jinou adresu
  • C validuje SPF politiku a poštu, u které SPF selhává, tvrdě odmítá

Problém nastane ve chvíli, kdy uživatel schránky u B nastaví přesměrování všech nebo vybraných zpráv od A na adresu u C. K C se totiž v okamžiku přeposílání připojí server entity B a bude se snažit doručit zprávu která bude v SMTP komunikaci MAIL FROM označena jako přicházející od A. To C vyhodnotí jako porušení politiky a zprávu odmítne.

Zpráva se tedy nedoručí a není to ničí vina. Každá z entit však může nějakým způsobem proces ovlivnit s cílem snížit riziko takového nedoručení. Entita A má možnost použít kvalifikátor softfail namísto fail (tedy zakončit definici politiky ~all namísto -all). Dává tím najevo, že si sice nepřeje, aby jiné adresy odesílaly poštu jejím jménem, ale nepřeje si tvrdé odmítnutí takových zpráv. Stejně tak entita C může nastavit svůj systém tak, aby i zprávy, jejichž SPF kontrola selže, podrobila pouze důkladnější kontrole, ale neodmítala. Standard definující SPF totiž záměrně nechává volnost v tom, co se má se zprávou na základě výsledku kontroly stát.

I entita B může přeposílání upravit tak, aby k porušování SPF politiky nedocházelo. Zdaleka nejjednodušší, nikoli však správné řešení, je přeposílání zpráv s prázdnou obálkovou adresou odesílatele. Negativním efektem takového řešení je, že odesílatel původní zprávy A se nedozví o případných problémech s doručením zprávy od BC. Další spíše teoretickou možností, kterou připouští SPF standard, je odmítnutí zprávy, jejíž přeposlání by porušilo SPF politiku, návratovým kódem 551, User not local spolu s uvedením adresy, na kterou má být zpráva odeslána přímo původním odesílatelem.

Přepisování adresy odesílatele podle SRS

Poslední možností, jak může entita B zabránit porušování SPF politiky, je systematické přepisování obálkové adresy odesílatele během přeposílání tak, aby případné informace o průběhu doručování k C, bylo možné předat zpět odesílateli. Nelze to však udělat jednoduchým přepisem; takové řešení by ze serveru B efektivně vytvářelo open relay, který by mohl být zneužit k rozesílání spamu. Metoda Sender Rewriting Scheme přepisované adresy proti zneužití zabezpečuje buď kryptograficky, nebo použitím databáze.

V prvním případě je pro odesílatele [email protected], posílajícího zprávu na server B.org tímto serverem při přeposílání vytvořena následující obálková adresa odesílatele:

[email protected] 

Písmena TT jsou nahrazena časovou značkou, kdy byla přepsaná adresa použita, písmena HHH pak jsou nahrazena částí hashe, na jehož vstupu je adresa odesílatele, časový kód a náhodné tajemství, známé pouze serverům, které generují a validují přepsané adresy.

Protože zpráva ze serveru entity B přichází s obálkovou adresou entity B, nepředstavuje její doručení žádný problém v SPF. Pokud dojde k problému s doručením přeposlané zprávy, je informace o problému poslána na výše uvedenou speciální adresu. Server B ze speciální adresy extrahuje původní adresu [email protected], ověří, zda souhlasí hash a zda od časové značky neuplynula příliš dlouhá doba, a pokud vše souhlasí, zprávu předá původnímu odesílateli.

Takovýto přepis má jistá úskalí, související především s maximální povolenou délkou uživatelské částí e-mailové adresy, kterou RFC 5321 stanovuje na 64 znaků. Zejména pak v případě, kdy k přeposílání a SRS přepisu dojde víc než jedenkrát. Z toho důvodu je pro takové případy definován zjednodušený formát:

[email protected] 

Písmena KKK představují nový hash, který vytvoří server B2.org. Při třetím a dalším přeposlání se již formát přepsané adresy nemění – případné zprávy jsou přeposlány přímo serveru, který provedl první přepis a od něj původnímu odesílateli.

Jinou možností přepisu adres je použití databáze. Každá adresa odesílatele se uloží do databáze pod náhodně vygenerovaným klíčem. Adresa se pak přepíše do tvaru:

SRS0=<klíč>@B.org 

Při doručení zprávy na takovou adresu se skutečná identita odesílatele zjistí z databáze. Položky se z databáze se po určité době vyčistí, aby nebylo možné jednou naučenou adresu používat trvale. Výhodou použití databáze je hlavně omezení problémů s délkou uživatelské části e-mailové adresy. Nevýhodou naopak nutnost databázi synchronizovat a sdílet mezi všemi poštovními servery organizace.

Ne úplně povedený standard

S trochou nadsázky to s odstupem času vypadá, že standard SPF řeší zhruba stejné množství problémů, jako sám vytváří. Velcí e-mailoví hráči ho však vzali za svůj, a tak zbytku správců nezbývá než se přizpůsobit. Máte-li v plánu SPF zavést pro svoji doménu, začněte nejdříve průzkumem, kudy uživatelé e-mailových schránek odesílají svou poštu a případně nápravou špatného stavu. Stále totiž existuje nemalé procento lidí, kteří odesílají poštu pochybnými cestami začínajícími obvykle u SMTP serveru internetového poskytovatele. Situace se ale pomalu zlepšuje, i velcí freemailoví poskytovatelé nabízejí autentizované předávání pošty k tomu určenou službou Submission (TCP/587).

Vzhledem k problému s přeposíláním rozhodně nelze doporučit zavádět tvrdé -all a doufat, že problém bude vyřešen na jiné straně spojení. Volba softfail se jeví jako nanejvýš vhodná.

zabbix_tip

Už z popisu SRS je jasné, že jde o vcelku komplexní službu, která navíc není nativně podporována většinou poštovních serverů. O jejím nasazení se však vyplatí uvažovat zejména u nejrůznějších školních či univerzitních schránek, kde je míra přeposílání pošty velká.

Nasadili jste SPF a/nebo SRS ve své síti? Podělte se s ostatními čtenáři o zkušenosti s použitým softwarem v diskuzi!

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 »