[markdown] replacing the transient token with a randomized page name for JCEF preview (IJPL-187567)

(cherry picked from commit 1deea1b9794b926a7aaa3b1ec2d244391fd4e10c)

IJ-CR-170152

GitOrigin-RevId: ea41949904247a6eb586eec1a1ecace48b48daba
This commit is contained in:
Roman Shevchenko
2025-07-23 09:32:24 +02:00
committed by intellij-monorepo-bot
parent 842623e56e
commit bac6daff8b
2 changed files with 33 additions and 68 deletions

View File

@@ -1,20 +1,18 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.plugins.markdown.ui.preview
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.util.text.StringUtil
import com.intellij.util.Urls.parseEncoded
import com.intellij.util.Urls
import io.netty.buffer.Unpooled
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import io.netty.handler.codec.http.*
import org.jetbrains.ide.BuiltInServerManager.Companion.getInstance
import org.jetbrains.ide.BuiltInServerManager
import org.jetbrains.ide.HttpRequestHandler
import org.jetbrains.io.FileResponses.checkCache
import org.jetbrains.io.FileResponses.getContentType
import org.jetbrains.io.send
import java.net.URL
import java.util.*
/**
@@ -54,17 +52,12 @@ class PreviewStaticServer : HttpRequestHandler() {
return synchronized(resourceProviders) { resourceProviders.getOrDefault(providerHash, null) }
}
override fun isSupported(request: FullHttpRequest): Boolean {
if (!super.isSupported(request)) {
return false
}
val path = request.uri()
return path.startsWith(prefixPath)
}
override fun isSupported(request: FullHttpRequest): Boolean =
super.isSupported(request) && request.uri().startsWith(ENDPOINT_PREFIX_PATH)
override fun process(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): Boolean {
val path = urlDecoder.path()
check(path.startsWith(prefixPath)) { "prefix should have been checked by #isSupported" }
check(path.startsWith(ENDPOINT_PREFIX_PATH)) { "prefix should have been checked by #isSupported" }
val resourceProvider = obtainResourceProvider(path) ?: return false
val resourceName = getStaticPath(path)
if (resourceProvider.canProvide(resourceName)) {
@@ -80,25 +73,21 @@ class PreviewStaticServer : HttpRequestHandler() {
}
companion object {
private const val endpointPrefix = "markdownPreview"
private const val prefixPath = "/$endpointPrefix"
private const val ENDPOINT_PREFIX = "markdownPreview"
private const val ENDPOINT_PREFIX_PATH = "/${ENDPOINT_PREFIX}"
@JvmStatic
val instance: PreviewStaticServer
get() = EP_NAME.findExtension(PreviewStaticServer::class.java) ?: error("Could not get server instance!")
@JvmStatic
fun createCSP(scripts: List<String>, styles: List<String>): String {
// We need to remove any query parameters to stop annoying errors in the browser console
fun stripQueryParameters(url: String) = url.replace("?${URL(url).query}", "")
return """
default-src 'none';
script-src ${StringUtil.join(scripts.map(::stripQueryParameters), " ")};
style-src https: ${StringUtil.join(styles.map(::stripQueryParameters), " ")} 'unsafe-inline';
img-src file: * data:; connect-src 'none'; font-src * data: *;
object-src 'none'; media-src 'none'; child-src 'none';
"""
}
internal fun createCSP(scripts: List<String>, styles: List<String>): String = """
default-src 'none';
script-src ${scripts.joinToString(" ")};
style-src https: ${styles.joinToString(" ")} 'unsafe-inline';
img-src file: * data:; connect-src 'none'; font-src * data: *;
object-src 'none'; media-src 'none'; child-src 'none';
"""
/**
* Expected to return same URL on each call for same [resourceProvider] and [staticPath],
@@ -107,11 +96,10 @@ class PreviewStaticServer : HttpRequestHandler() {
@JvmStatic
fun getStaticUrl(resourceProvider: ResourceProvider, staticPath: String): String {
val providerHash = resourceProvider.hashCode()
val port = getInstance().port
val raw = "http://localhost:$port/$endpointPrefix/$providerHash/$staticPath"
val url = parseEncoded(raw)
requireNotNull(url) { "Could not parse url!" }
return getInstance().addAuthToken(url).toExternalForm()
val port = BuiltInServerManager.getInstance().port
val raw = "http://localhost:${port}/${ENDPOINT_PREFIX}/${providerHash}/${staticPath}"
requireNotNull(Urls.parseEncoded(raw)) { "Invalid URL: ${raw}" }
return raw
}
/**

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.plugins.markdown.ui.preview.jcef
import com.intellij.ide.ui.UISettingsListener
@@ -22,6 +22,7 @@ import com.intellij.ui.jcef.JBCefApp
import com.intellij.ui.jcef.JBCefClient
import com.intellij.ui.jcef.JCEFHtmlPanel
import com.intellij.util.application
import com.intellij.util.io.DigestUtil
import com.intellij.util.net.NetUtils
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
@@ -58,13 +59,10 @@ class MarkdownJCEFHtmlPanel(
) : JCEFHtmlPanel(isOffScreenRendering(), null, null), MarkdownHtmlPanelEx, UserDataHolder by UserDataHolderBase() {
constructor() : this(null, null)
private val pageBaseName = "markdown-preview-index-${hashCode()}.html"
private val pageBaseName = "markdown-preview-index-${DigestUtil.randomToken()}.html"
private val resourceProvider = MyAggregatingResourceProvider()
private val browserPipe: BrowserPipe = JcefBrowserPipeImpl(
this,
injectionAllowedUrls = listOf(PreviewStaticServer.getStaticUrl(resourceProvider, pageBaseName))
)
private val pageUrl = PreviewStaticServer.getStaticUrl(resourceProvider, pageBaseName)
private val browserPipe: BrowserPipe = JcefBrowserPipeImpl(browser = this, injectionAllowedUrls = listOf(pageUrl))
private val scrollListeners = ArrayList<MarkdownHtmlPanel.ScrollListener>()
@@ -78,40 +76,21 @@ class MarkdownJCEFHtmlPanel(
.sorted()
}
private val scripts
get() = baseScripts + currentExtensions.flatMap { it.scripts }
private val styles
get() = currentExtensions.flatMap { it.styles }
private val scriptingLines
get() = scripts.joinToString("\n") {
"<script src=\"${PreviewStaticServer.getStaticUrl(resourceProvider, it)}\"></script>"
}
private val stylesLines
get() = styles.joinToString("\n") {
"<link rel=\"stylesheet\" href=\"${PreviewStaticServer.getStaticUrl(resourceProvider, it)}\"/>"
}
private val contentSecurityPolicy get() = PreviewStaticServer.createCSP(
scripts.map { PreviewStaticServer.getStaticUrl(resourceProvider, it) },
styles.map { PreviewStaticServer.getStaticUrl(resourceProvider, it) }
)
private val updateHandler = MarkdownUpdateHandler.Debounced()
private fun buildIndexContent(): String {
val scripts = (baseScripts + currentExtensions.flatMap { it.scripts }).map { PreviewStaticServer.getStaticUrl(resourceProvider, it) }
val styles = currentExtensions.flatMap { it.styles }.map { PreviewStaticServer.getStaticUrl(resourceProvider, it) }
// language=HTML
return """
<!DOCTYPE html>
<html>
<head>
<title>IntelliJ Markdown Preview</title>
<meta http-equiv="Content-Security-Policy" content="$contentSecurityPolicy"/>
<meta http-equiv="Content-Security-Policy" content="${PreviewStaticServer.createCSP(scripts, styles)}"/>
<meta name="markdown-position-attribute-name" content="${HtmlGenerator.SRC_ATTRIBUTE_NAME}"/>
$scriptingLines
$stylesLines
${scripts.joinToString("\n") { "<script src=\"${it}\"></script>" }}
${styles.joinToString("\n") { "<link rel=\"stylesheet\" href=\"${it}\"/>" }}
</head>
</html>
"""
@@ -119,7 +98,7 @@ class MarkdownJCEFHtmlPanel(
private suspend fun loadIndexContent() {
reloadExtensions()
waitForPageLoad(PreviewStaticServer.getStaticUrl(resourceProvider, pageBaseName))
waitForPageLoad(pageUrl)
}
private var previousRenderClosure: String = ""
@@ -420,13 +399,11 @@ class MarkdownJCEFHtmlPanel(
""".trimIndent())
return true
}
val targetPageUrl = PreviewStaticServer.getStaticUrl(resourceProvider, pageBaseName)
val requestedUrl = request.url
if (requestedUrl != targetPageUrl) {
if (request.url != pageUrl) {
logger.warn("""
Canceling request for an external page with url: $requestedUrl.
Canceling request for an external page with url: ${request.url}.
Current page url: ${browser.url}
Target safe url: $targetPageUrl
Target safe url: ${pageUrl}
""".trimIndent())
return true
}