From 0781396c95a79d4ea8ce1a7d5f34eb81237c9ed3 Mon Sep 17 00:00:00 2001 From: Arne Babenhauserheide Date: Sat, 23 Jan 2021 16:08:33 +0100 Subject: [PATCH 1/5] [m3u-player] inject vanilla Javascript m3u-player into Freesites With this change, adding a media-tag that references an m3u-list like or into a Freesite will turn the media-tag into an m3u-player that dynamically loads consecutive entries from the m3u file as needed to provide continuous play. --- src/freenet/client/filter/HTMLFilter.java | 38 ++- .../clients/http/ToadletContextImpl.java | 29 +- .../clients/http/staticfiles/js/m3u-player.js | 249 ++++++++++++++++++ 3 files changed, 310 insertions(+), 6 deletions(-) create mode 100644 src/freenet/clients/http/staticfiles/js/m3u-player.js diff --git a/src/freenet/client/filter/HTMLFilter.java b/src/freenet/client/filter/HTMLFilter.java index 95b716ffdf5..6e091ebfe28 100644 --- a/src/freenet/client/filter/HTMLFilter.java +++ b/src/freenet/client/filter/HTMLFilter.java @@ -43,6 +43,7 @@ public class HTMLFilter implements ContentDataFilter, CharsetExtractor { + public static final String M3U_PLAYER_TAG_FILE = "freenet/clients/http/staticfiles/js/m3u-player.js"; private static boolean logMINOR; private static boolean logDEBUG; @@ -61,6 +62,8 @@ public class HTMLFilter implements ContentDataFilter, CharsetExtractor { /** -1 means don't allow it */ public static int metaRefreshRedirectMinInterval = 30; + private static final String m3uPlayerScriptTagContent = m3uPlayerScriptTagContent(); + @Override public void readFilter( InputStream input, OutputStream output, String charset, Map otherParams, @@ -559,6 +562,30 @@ else if((c < 32) && (c != '\t') && (c != '\n') && (c != '\r')) { w.write(sout); } + static String m3uPlayerScriptTagContent() { + InputStream m3uPlayerTagStream = HTMLFilter.class.getClassLoader() + .getResourceAsStream(M3U_PLAYER_TAG_FILE); + String errorTag = "/* Error: could not load " + M3U_PLAYER_TAG_FILE + " */"; + if (m3uPlayerTagStream == null) { + return errorTag; + } + String tagContent; + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(m3uPlayerTagStream))) { + StringBuilder stringBuilder = new StringBuilder(""); + tagContent = stringBuilder.toString(); + } catch (IOException e) { + Logger.error(HTMLFilter.class, "Could not read m3uPlayer inline-script."); + return errorTag; + } + return tagContent; + } + String processTag(List splitTag, Writer w, HTMLParseContext pc) throws IOException, DataFilterException { // First, check that it is a recognized tag @@ -578,6 +605,7 @@ String processTag(List splitTag, Writer w, HTMLParseContext pc) if(t.element.compareTo("head")==0 && !t.startSlash){ pc.wasHeadElementFound=true; } else if(t.element.compareTo("head")==0 && t.startSlash) { + w.write(m3uPlayerScriptTagContent); pc.headEnded = true; if(pc.onlyDetectingCharset) pc.failedDetectCharset = true; //If we found a or a <meta> without a <head>, then we need to add them to a <head> @@ -592,7 +620,10 @@ String processTag(List<String> splitTag, Writer w, HTMLParseContext pc) throwFilterException(l10n("metaOutsideHead")); //If we found a <body> and haven't closed <head> already, then we do }else if(t.element.compareTo("body") == 0 && pc.openElements.contains("head")){ - if(!pc.onlyDetectingCharset) w.write("</head>"); + if(!pc.onlyDetectingCharset) { + w.write(m3uPlayerScriptTagContent); + w.write("</head>"); + } pc.headEnded = true; if(pc.onlyDetectingCharset) pc.failedDetectCharset = true; pc.openElements.pop(); @@ -601,7 +632,10 @@ String processTag(List<String> splitTag, Writer w, HTMLParseContext pc) pc.wasHeadElementFound=true; String headContent=pc.cb.processTag(new ParsedTag("head", new HashMap<String, String>())); if(headContent!=null){ - if(!pc.onlyDetectingCharset) w.write(headContent+"</head>"); + if(!pc.onlyDetectingCharset) { + w.write(m3uPlayerScriptTagContent); + w.write(headContent+"</head>"); + } pc.headEnded = true; if(pc.onlyDetectingCharset) pc.failedDetectCharset = true; } diff --git a/src/freenet/clients/http/ToadletContextImpl.java b/src/freenet/clients/http/ToadletContextImpl.java index c6a4f514b5f..cdc205225e7 100644 --- a/src/freenet/clients/http/ToadletContextImpl.java +++ b/src/freenet/clients/http/ToadletContextImpl.java @@ -18,7 +18,9 @@ import java.util.ArrayList; import java.util.Date; import java.util.Enumeration; +import java.util.List; import java.util.Locale; +import java.util.StringJoiner; import java.util.TimeZone; import freenet.clients.http.FProxyFetchInProgress.REFILTER_POLICY; @@ -445,9 +447,12 @@ static void sendReplyHeaders(OutputStream sockOutputStream, int replyCode, Strin private static String generateCSP(boolean allowScripts, boolean allowFrames) { StringBuilder sb = new StringBuilder(); - sb.append("default-src 'self'; script-src "); + // allow access to blobs, because these are purely local + sb.append("default-src 'self' blob:; script-src "); // "options inline-script" is old syntax needed for older Firefox's. - sb.append(allowScripts ? "'self' 'unsafe-inline'; options inline-script" : "'none'"); + sb.append(allowScripts + ? "'self' 'unsafe-inline'; options inline-script" + : generateRestrictedScriptSrc()); sb.append("; frame-src "); sb.append(allowFrames ? "'self'" : "'none'"); sb.append("; object-src 'none'"); @@ -457,8 +462,24 @@ private static String generateCSP(boolean allowScripts, boolean allowFrames) { return sb.toString(); } - static TimeZone TZ_UTC = TimeZone.getTimeZone("UTC"); - + private static String generateRestrictedScriptSrc() { + // TODO: auto-generate these hashes from the path to the source file + String[] allowedScriptHashes = new String[] { + "sha256-xWSdtptsjJ8xJwS8jRAreqx8NAk7ofR+v/QfIcv/P7Q=" // freenet/clients/http/staticfiles/js/m3u-player.js + }; + if (allowedScriptHashes.length == 0) { + return "'none'"; + } else { + StringJoiner stringJoiner = new StringJoiner("' '", "'", "'"); + for (String source : allowedScriptHashes) { + stringJoiner.add(source); + } + return stringJoiner.toString(); + } + } + + static TimeZone TZ_UTC = TimeZone.getTimeZone("UTC"); + public static Date parseHTTPDate(String httpDate) throws java.text.ParseException{ SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'",Locale.US); sdf.setTimeZone(TZ_UTC); diff --git a/src/freenet/clients/http/staticfiles/js/m3u-player.js b/src/freenet/clients/http/staticfiles/js/m3u-player.js new file mode 100644 index 00000000000..e0c4d48f1b8 --- /dev/null +++ b/src/freenet/clients/http/staticfiles/js/m3u-player.js @@ -0,0 +1,249 @@ +// [[file:m3u-player.org::*The script][The script:1]] +// @license magnet:?xt=urn:btih:cf05388f2679ee054f2beb29a391d25f4e673ac3&dn=gpl-2.0.txt GPL-v2-or-Later +// When changing even a single letter in this file, you MUST adjust +// freenet.clients.http.ToadletContextImpl.generateRestrictedScriptSrc, +// otherwise the CSP policy will prevent loading this script. +// use the following shell-command to get the new hash: +// sha256sum m3u-player.js | cut -d " " -f 1 | xxd -r -p | base64 +// AVOID unicode characters. They may change on the way to the +// browser, invalidating the CSP header. +// Also you MUST escape any ampersand and less-than or greater-than signs, +// because this file will be inlined into the site. +const playlists = {}; +const prefetchedTracks = new Map(); // use a map for insertion order, so we can just blow away old entries. +// maximum prefetched blobs that are kept. +const MAX_PREFETCH_KEEP = 10; +// maximum allowed number of entries in a playlist to prevent OOM attacks against the browser with self-referencing playlists +const MAX_PLAYLIST_LENGTH = 1000; +const PLAYLIST_MIME_TYPES = ["audio/x-mpegurl", "audio/mpegurl", "application/vnd.apple.mpegurl","application/mpegurl","application/x-mpegurl"]; +function stripUrlParameters(link) { + const url = new URL(link, window.location); + url.search = ""; + url.hash = ""; + return url.href; +} +function isPlaylist(link) { + const linkHref = stripUrlParameters(link); + return linkHref.endsWith(".m3u") || linkHref.endsWith(".m3u8"); +} +function isBlob(link) { + return new URL(link, window.location).protocol == 'blob'; +} +/** + * Replace the host of the link with the host seen by the browser to get around CSP restrictions. + * This must be applied to all links read from anywhere. + * invalid links will fail at the URL constructor. + * + * Keep in mind that the link here is user input, though already + * filtered by the m3u-filter (we check against the + * PLAYLIST_MIME_TYPES whether Freenet saw this as m3u-list), + * and all user input is potentially evil. + */ +function replaceHost(link) { + const url = new URL(link, window.location); + if (url.protocol !== 'blob:') { + url.host = window.location.host; + url.port = window.location.port; + } + return url.href; +} +function parsePlaylist(textContent) { + return textContent.match(/^(?!#)(?!\s).*$/mg) + .filter(s => s) // filter removes empty strings + .map(replaceHost); // now all tracks point to our local installation. The m3u-filter ensures that they are valid keys. +} +/** + * Download the given playlist, parse it, and store the tracks in the + * global playlists object using the url as key. + * + * Runs callback once the playlist downloaded successfully. + */ +function fetchPlaylist(url, onload, onerror) { + const playlistFetcher = new XMLHttpRequest(); + playlistFetcher.open("GET", url, true); + playlistFetcher.responseType = "blob"; // to get a mime type + playlistFetcher.onload = () => { + if (PLAYLIST_MIME_TYPES.includes(playlistFetcher.response.type)) { // security check to ensure that filters have run + const reader = new FileReader(); + const load = onload; // propagate to inner scope + reader.addEventListener("loadend", e => { + playlists[url] = parsePlaylist(reader.result); + onload(); + }); + reader.readAsText(playlistFetcher.response); + } else { + console.error("playlist must have one of the playlist MIME type '" + PLAYLIST_MIME_TYPES + "' but it had MIME type '" + playlistFetcher.response.type + "'."); + onerror(); + } + }; + playlistFetcher.onerror = onerror; + playlistFetcher.abort = onerror; + playlistFetcher.send(); +} +function prefetchTrack(url, onload) { + if (prefetchedTracks.has(url)) { + return; + } + // first cleanup: kill the oldest entries until we're back at the allowed size + while (prefetchedTracks.size > MAX_PREFETCH_KEEP) { + const key = prefetchedTracks.keys().next().value; + const track = prefetchedTracks.get(key); + prefetchedTracks.delete(key); + } + // first set the prefetched to the url so we will never request twice + prefetchedTracks.set(url, url); + // now start replacing it with a blob + const xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); + xhr.responseType = "blob"; + xhr.onload = () => { + prefetchedTracks.set(url, xhr.response); + if (onload) { + onload(); + } + }; + xhr.send(); +} +function updateSrc(mediaTag, callback) { + const playlistUrl = mediaTag.getAttribute("playlist"); + const trackIndex = mediaTag.getAttribute("track-index"); + // deepcopy playlists to avoid shared mutation + let playlist = [...playlists[playlistUrl]]; + let trackUrl = playlist[trackIndex]; + // strip out the host; we do not need that in Freenet + // download and splice in playlists as needed + if (isPlaylist(trackUrl)) { + if (playlist.length >= MAX_PLAYLIST_LENGTH) { + // skip playlist if we already have too many tracks + changeTrack(mediaTag, +1); + } else { + // do not use the cached playlist here, though it is tempting: it might genuinely change to allow for updates + fetchPlaylist( + trackUrl, + () => { + playlist.splice(trackIndex, 1, ...playlists[trackUrl]); + playlists[playlistUrl] = playlist; + updateSrc(mediaTag, callback); + }, + () => callback()); + } + } else { + let url = prefetchedTracks.has(trackUrl) + ? prefetchedTracks.get(trackUrl) instanceof Blob + ? URL.createObjectURL(prefetchedTracks.get(trackUrl)) + : trackUrl : trackUrl; + const oldUrl = mediaTag.getAttribute("src"); + mediaTag.setAttribute("src", url); + // replace the url when done, because a blob from an xhr request + // is more reliable in the media tag; + // the normal URL caused jumping prematurely to the next track. + if (url == trackUrl) { + prefetchTrack(trackUrl, () => { + if (mediaTag.paused) { + if (url == mediaTag.getAttribute("src")) { + if (mediaTag.currentTime === 0) { + mediaTag.setAttribute("src", URL.createObjectURL( + prefetchedTracks.get(url))); + } + } + } + }); + } + // allow releasing memory + if (isBlob(oldUrl)) { + URL.revokeObjectURL(oldUrl); + } + // update title + mediaTag.parentElement.querySelector(".m3u-player--title").title = stripUrlParameters(trackUrl); + mediaTag.parentElement.querySelector(".m3u-player--title").textContent = stripUrlParameters(trackUrl); + // start prefetching the next two tracks. + for (const i of [1, 2]) { + if (playlist.length > Number(trackIndex) + i) { + prefetchTrack(playlist[Number(trackIndex) + i]); + } + } + callback(); + } +} +function changeTrack(mediaTag, diff) { + const currentTrackIndex = Number(mediaTag.getAttribute("track-index")); + const nextTrackIndex = currentTrackIndex + diff; + const tracks = playlists[mediaTag.getAttribute("playlist")]; + if (nextTrackIndex >= 0) { // do not collapse the if clauses with double-and, that does not survive inlining + if (tracks.length > nextTrackIndex) { + mediaTag.setAttribute("track-index", nextTrackIndex); + updateSrc(mediaTag, () => mediaTag.play()); + } + } +} + +/** + * Turn a media tag into playlist player. + */ +function initPlayer(mediaTag) { + mediaTag.setAttribute("playlist", replaceHost(mediaTag.getAttribute("src"))); + mediaTag.setAttribute("track-index", 0); + const url = mediaTag.getAttribute("playlist"); + const wrapper = mediaTag.parentElement.insertBefore(document.createElement("div"), mediaTag); + const controls = document.createElement("div"); + const left = document.createElement("span"); + const title = document.createElement("span"); + const right = document.createElement("span"); + controls.appendChild(left); + controls.appendChild(title); + controls.appendChild(right); + left.classList.add("m3u-player--left"); + right.classList.add("m3u-player--right"); + title.classList.add("m3u-player--title"); + title.style.overflow = "hidden"; + title.style.textOverflow = "ellipsis"; + title.style.whiteSpace = "nowrap"; + title.style.opacity = "0.3"; + title.style.direction = "rtl"; // for truncation on the left + title.style.paddingLeft = "0.5em"; + title.style.paddingRight = "0.5em"; + controls.style.display = "flex"; + controls.style.justifyContent = "space-between"; + const styleTag = document.createElement("style"); + styleTag.innerHTML = ".m3u-player--left:hover, .m3u-player--right:hover {color: wheat; background-color: DarkSlateGray}"; + wrapper.appendChild(styleTag); + wrapper.appendChild(controls); + controls.style.width = mediaTag.getBoundingClientRect().width.toString() + "px"; + // appending the media tag to the wrapper removes it from the outer scope but keeps the event listeners + wrapper.appendChild(mediaTag); + left.innerHTML = "<"; // not textContent, because we MUST escape + // the tag here and textContent shows the + // escaped version + left.onclick = () => changeTrack(mediaTag, -1); + right.innerHTML = ">"; + right.onclick = () => changeTrack(mediaTag, +1); + fetchPlaylist( + url, + () => { + updateSrc(mediaTag, () => null); + mediaTag.addEventListener("ended", event => { + if (mediaTag.currentTime >= mediaTag.duration) { + changeTrack(mediaTag, +1); + } + }); + }, + () => null); + // keep the controls aligned to the media tag + mediaTag.resizeObserver = new ResizeObserver(entries => { + controls.style.width = entries[0].contentRect.width.toString() + "px"; + }); + mediaTag.resizeObserver.observe(mediaTag); +} +function processTag(mediaTag) { + if (!mediaTag.canPlayType('audio/x-mpegurl')) { + if (isPlaylist(mediaTag.getAttribute("src"))) { + initPlayer(mediaTag); + } + } +} +document.addEventListener('DOMContentLoaded', () => { + const nodes = document.querySelectorAll("audio,video"); + nodes.forEach(processTag); +}); +// @license-end +// The script:1 ends here From b9c3c2f36a8960c545cdb61c60c6208b3029d058 Mon Sep 17 00:00:00 2001 From: Arne Babenhauserheide <arne_bab@web.de> Date: Sat, 23 Jan 2021 16:40:05 +0100 Subject: [PATCH 2/5] [m3u-player] adjust ContentFilterTest: header with script-source --- .../client/filter/ContentFilterTest.java | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/test/freenet/client/filter/ContentFilterTest.java b/test/freenet/client/filter/ContentFilterTest.java index 4d6c95a1099..67e30b5c13f 100644 --- a/test/freenet/client/filter/ContentFilterTest.java +++ b/test/freenet/client/filter/ContentFilterTest.java @@ -213,36 +213,36 @@ public void testHTMLFilter() throws Exception { public void testMetaRefresh() throws Exception { HTMLFilter.metaRefreshSamePageMinInterval = 5; HTMLFilter.metaRefreshRedirectMinInterval = 30; - assertEquals(META_TIME_ONLY, headFilter(META_TIME_ONLY)); - assertEquals(META_TIME_ONLY, headFilter(META_TIME_ONLY_WRONG_CASE)); - assertEquals(META_TIME_ONLY, headFilter(META_TIME_ONLY_TOO_SHORT)); - assertEquals("", headFilter(META_TIME_ONLY_NEGATIVE)); - assertEquals(META_TIME_ONLY_BADNUM_OUT, headFilter(META_TIME_ONLY_BADNUM1)); - assertEquals(META_TIME_ONLY_BADNUM_OUT, headFilter(META_TIME_ONLY_BADNUM2)); - assertEquals(META_VALID_REDIRECT, headFilter(META_VALID_REDIRECT)); - assertEquals(META_VALID_REDIRECT, headFilter(META_VALID_REDIRECT_NOSPACE)); - assertEquals(META_BOGUS_REDIRECT1_OUT, headFilter(META_BOGUS_REDIRECT1)); - assertEquals(META_BOGUS_REDIRECT1_OUT, headFilter(META_BOGUS_REDIRECT2)); - assertEquals(META_BOGUS_REDIRECT3_OUT, headFilter(META_BOGUS_REDIRECT3)); - assertEquals(META_BOGUS_REDIRECT4_OUT, headFilter(META_BOGUS_REDIRECT4)); - assertEquals(META_BOGUS_REDIRECT1_OUT, headFilter(META_BOGUS_REDIRECT5)); - assertEquals(META_BOGUS_REDIRECT_NO_URL, headFilter(META_BOGUS_REDIRECT6)); + assertEquals(META_TIME_ONLY + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_TIME_ONLY)); + assertEquals(META_TIME_ONLY + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_TIME_ONLY_WRONG_CASE)); + assertEquals(META_TIME_ONLY + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_TIME_ONLY_TOO_SHORT)); + assertEquals("" + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_TIME_ONLY_NEGATIVE)); + assertEquals(META_TIME_ONLY_BADNUM_OUT + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_TIME_ONLY_BADNUM1)); + assertEquals(META_TIME_ONLY_BADNUM_OUT + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_TIME_ONLY_BADNUM2)); + assertEquals(META_VALID_REDIRECT + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_VALID_REDIRECT)); + assertEquals(META_VALID_REDIRECT + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_VALID_REDIRECT_NOSPACE)); + assertEquals(META_BOGUS_REDIRECT1_OUT + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_BOGUS_REDIRECT1)); + assertEquals(META_BOGUS_REDIRECT1_OUT + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_BOGUS_REDIRECT2)); + assertEquals(META_BOGUS_REDIRECT3_OUT + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_BOGUS_REDIRECT3)); + assertEquals(META_BOGUS_REDIRECT4_OUT + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_BOGUS_REDIRECT4)); + assertEquals(META_BOGUS_REDIRECT1_OUT + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_BOGUS_REDIRECT5)); + assertEquals(META_BOGUS_REDIRECT_NO_URL + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_BOGUS_REDIRECT6)); HTMLFilter.metaRefreshSamePageMinInterval = -1; HTMLFilter.metaRefreshRedirectMinInterval = -1; - assertEquals("", headFilter(META_TIME_ONLY)); - assertEquals("", headFilter(META_TIME_ONLY_WRONG_CASE)); - assertEquals("", headFilter(META_TIME_ONLY_TOO_SHORT)); - assertEquals("", headFilter(META_TIME_ONLY_NEGATIVE)); - assertEquals("", headFilter(META_TIME_ONLY_BADNUM1)); - assertEquals("", headFilter(META_TIME_ONLY_BADNUM2)); - assertEquals("", headFilter(META_VALID_REDIRECT)); - assertEquals("", headFilter(META_VALID_REDIRECT_NOSPACE)); - assertEquals("", headFilter(META_BOGUS_REDIRECT1)); - assertEquals("", headFilter(META_BOGUS_REDIRECT2)); - assertEquals("", headFilter(META_BOGUS_REDIRECT3)); - assertEquals("", headFilter(META_BOGUS_REDIRECT4)); - assertEquals("", headFilter(META_BOGUS_REDIRECT5)); - assertEquals("", headFilter(META_BOGUS_REDIRECT6)); + assertEquals("" + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_TIME_ONLY)); + assertEquals("" + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_TIME_ONLY_WRONG_CASE)); + assertEquals("" + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_TIME_ONLY_TOO_SHORT)); + assertEquals("" + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_TIME_ONLY_NEGATIVE)); + assertEquals("" + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_TIME_ONLY_BADNUM1)); + assertEquals("" + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_TIME_ONLY_BADNUM2)); + assertEquals("" + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_VALID_REDIRECT)); + assertEquals("" + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_VALID_REDIRECT_NOSPACE)); + assertEquals("" + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_BOGUS_REDIRECT1)); + assertEquals("" + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_BOGUS_REDIRECT2)); + assertEquals("" + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_BOGUS_REDIRECT3)); + assertEquals("" + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_BOGUS_REDIRECT4)); + assertEquals("" + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_BOGUS_REDIRECT5)); + assertEquals("" + HTMLFilter.m3uPlayerScriptTagContent(), headFilter(META_BOGUS_REDIRECT6)); } private String headFilter(String data) throws Exception { From 3bfd9442d9a276ae8fd3bfca634e8eed83a9b996 Mon Sep 17 00:00:00 2001 From: Arne Babenhauserheide <arne_bab@web.de> Date: Sun, 24 Jan 2021 22:53:04 +0100 Subject: [PATCH 3/5] [m3u-player] prefetch the next three tracks to smooth segment-sizes When splitting video by silence there are too often two consecutive small segments. I did not see three consecutive small segments in the example stream, and this should work for the example stream. Further optimization can come with more experience with different streams. Example-Stream: SSK@UGh1rxaHczJVr4k4LwxWrxFBc-Dt5P0F3IuPONpp8ZQ,klWF5g9B2PLst8tRO8c9tzk6XvisGynFVJRzM8-9718,AQACAAE/stream-36c3-sff-5/ --- src/freenet/clients/http/ToadletContextImpl.java | 2 +- src/freenet/clients/http/staticfiles/js/m3u-player.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/freenet/clients/http/ToadletContextImpl.java b/src/freenet/clients/http/ToadletContextImpl.java index cdc205225e7..f24520686fd 100644 --- a/src/freenet/clients/http/ToadletContextImpl.java +++ b/src/freenet/clients/http/ToadletContextImpl.java @@ -465,7 +465,7 @@ private static String generateCSP(boolean allowScripts, boolean allowFrames) { private static String generateRestrictedScriptSrc() { // TODO: auto-generate these hashes from the path to the source file String[] allowedScriptHashes = new String[] { - "sha256-xWSdtptsjJ8xJwS8jRAreqx8NAk7ofR+v/QfIcv/P7Q=" // freenet/clients/http/staticfiles/js/m3u-player.js + "sha256-emGXuxNdTQP2ylJOeGhLCKYFO+1g/2u6FtPSzMKQ06A=" // freenet/clients/http/staticfiles/js/m3u-player.js }; if (allowedScriptHashes.length == 0) { return "'none'"; diff --git a/src/freenet/clients/http/staticfiles/js/m3u-player.js b/src/freenet/clients/http/staticfiles/js/m3u-player.js index e0c4d48f1b8..8ca19dff078 100644 --- a/src/freenet/clients/http/staticfiles/js/m3u-player.js +++ b/src/freenet/clients/http/staticfiles/js/m3u-player.js @@ -156,8 +156,8 @@ function updateSrc(mediaTag, callback) { // update title mediaTag.parentElement.querySelector(".m3u-player--title").title = stripUrlParameters(trackUrl); mediaTag.parentElement.querySelector(".m3u-player--title").textContent = stripUrlParameters(trackUrl); - // start prefetching the next two tracks. - for (const i of [1, 2]) { + // start prefetching the next three tracks. + for (const i of [1, 2, 3]) { if (playlist.length > Number(trackIndex) + i) { prefetchTrack(playlist[Number(trackIndex) + i]); } From db34d5a9a5b2e27152de6802fae2256099c312a1 Mon Sep 17 00:00:00 2001 From: Arne Babenhauserheide <arne_bab@web.de> Date: Sat, 13 Mar 2021 17:27:18 +0100 Subject: [PATCH 4/5] add config option for the m3u-player embedding --- src/freenet/client/filter/HTMLFilter.java | 16 ++++++++++++---- .../clients/http/SimpleToadletServer.java | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/freenet/client/filter/HTMLFilter.java b/src/freenet/client/filter/HTMLFilter.java index 6e091ebfe28..f4a643d6d19 100644 --- a/src/freenet/client/filter/HTMLFilter.java +++ b/src/freenet/client/filter/HTMLFilter.java @@ -43,7 +43,9 @@ public class HTMLFilter implements ContentDataFilter, CharsetExtractor { - public static final String M3U_PLAYER_TAG_FILE = "freenet/clients/http/staticfiles/js/m3u-player.js"; + private static final String M3U_PLAYER_TAG_FILE = "freenet/clients/http/staticfiles/js/m3u-player.js"; + /** if true, embed m3u player. Enabled when fproxy javascript is enabled. **/ + public static boolean embedM3uPlayer = true; private static boolean logMINOR; private static boolean logDEBUG; @@ -605,7 +607,9 @@ String processTag(List<String> splitTag, Writer w, HTMLParseContext pc) if(t.element.compareTo("head")==0 && !t.startSlash){ pc.wasHeadElementFound=true; } else if(t.element.compareTo("head")==0 && t.startSlash) { - w.write(m3uPlayerScriptTagContent); + if (embedM3uPlayer) { + w.write(m3uPlayerScriptTagContent); + } pc.headEnded = true; if(pc.onlyDetectingCharset) pc.failedDetectCharset = true; //If we found a <title> or a <meta> without a <head>, then we need to add them to a <head> @@ -621,7 +625,9 @@ String processTag(List<String> splitTag, Writer w, HTMLParseContext pc) //If we found a <body> and haven't closed <head> already, then we do }else if(t.element.compareTo("body") == 0 && pc.openElements.contains("head")){ if(!pc.onlyDetectingCharset) { - w.write(m3uPlayerScriptTagContent); + if (embedM3uPlayer) { + w.write(m3uPlayerScriptTagContent); + } w.write("</head>"); } pc.headEnded = true; @@ -633,7 +639,9 @@ String processTag(List<String> splitTag, Writer w, HTMLParseContext pc) String headContent=pc.cb.processTag(new ParsedTag("head", new HashMap<String, String>())); if(headContent!=null){ if(!pc.onlyDetectingCharset) { - w.write(m3uPlayerScriptTagContent); + if (embedM3uPlayer) { + w.write(m3uPlayerScriptTagContent); + } w.write(headContent+"</head>"); } pc.headEnded = true; diff --git a/src/freenet/clients/http/SimpleToadletServer.java b/src/freenet/clients/http/SimpleToadletServer.java index b39b271086f..ce5e09d7068 100644 --- a/src/freenet/clients/http/SimpleToadletServer.java +++ b/src/freenet/clients/http/SimpleToadletServer.java @@ -758,6 +758,21 @@ public void set(Integer val) }, false); HTMLFilter.metaRefreshRedirectMinInterval = Math.max(-1, fproxyConfig.getInt("metaRefreshRedirectInterval")); + fproxyConfig.register("embedM3uPlayerInFreesites", true, configItemOrder++, true, false, "SimpleToadletServer.embedM3uPlayerInFreesites", "SimpleToadletServer.embedM3uPlayerInFreesitesLong", + new BooleanCallback() { + + @Override + public Boolean get() { + return HTMLFilter.embedM3uPlayer; + } + + @Override + public void set(Boolean val) { + HTMLFilter.embedM3uPlayer = val; + } + }); + HTMLFilter.embedM3uPlayer = fproxyConfig.getBoolean("embedM3uPlayerInFreesites"); + fproxyConfig.register("refilterPolicy", "RE_FILTER", configItemOrder++, true, false, "SimpleToadletServer.refilterPolicy", "SimpleToadletServer.refilterPolicyLong", new ReFilterCallback()); From b0fc0c7bc42cdf6e3c060620b36e81df78a9063d Mon Sep 17 00:00:00 2001 From: Arne Babenhauserheide <arne_bab@web.de> Date: Sat, 13 Mar 2021 17:56:31 +0100 Subject: [PATCH 5/5] add l10n for config option for the m3u-player embedding --- src/freenet/l10n/freenet.l10n.en.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/freenet/l10n/freenet.l10n.en.properties b/src/freenet/l10n/freenet.l10n.en.properties index 15a491c3d7e..58fa08a7459 100644 --- a/src/freenet/l10n/freenet.l10n.en.properties +++ b/src/freenet/l10n/freenet.l10n.en.properties @@ -1915,6 +1915,8 @@ SimpleToadletServer.metaRefreshSamePageInterval=Allow freesites to refresh thems SimpleToadletServer.metaRefreshSamePageIntervalLong=Allow freesites to refresh themselves periodically with HTML meta refresh: Minimum interval in seconds or -1 for disabled. SimpleToadletServer.metaRefreshRedirectInterval=Allow freesites to redirect to other freesites after a delay: Minimum interval in seconds or -1 for disabled. SimpleToadletServer.metaRefreshRedirectIntervalLong=Allow freesites to redirect to other freesites after a delay with HTML meta refresh: Minimum interval in seconds or -1 for disabled. +SimpleToadletServer.embedM3uPlayerInFreesites=Embed an m3u player in Freesites? +SimpleToadletServer.embedM3uPlayerInFreesitesLong=Whether to embed a Javascript-based player checked by the Freenet team which turns video- and audio-tags with m3u-playlists into interactive streaming players. SimpleToadletServer.panicButton=Show the panic button? SimpleToadletServer.panicButtonLong=Shows a 'panic button' on the queue page that will remove all downloads and uploads, wipe the cache of recently visited freesites, and clear the master keys file. SimpleToadletServer.noConfirmPanic=No confirmation on panic button?