From b247ddfa9998c5f5dc61fb300169a73eee75f061 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 11 Apr 2016 13:39:04 +0200 Subject: [PATCH 01/67] add isAllowRequestOnlyFromLocalOrigin and enabled it by --- .../org/jetbrains/ide/HttpRequestHandler.java | 4 +++ .../BuiltInWebBrowserUrlProvider.java | 3 +- .../builtInWebServer/BuiltInWebServer.kt | 25 +---------------- .../org/jetbrains/ide/DiffHttpService.java | 5 ++++ .../jetbrains/ide/OpenFileHttpService.java | 5 ++++ .../ide/ProjectSetRequestHandler.java | 5 ++++ .../src/com/intellij/util/net/NetUtils.java | 8 +----- .../io/DelegatingHttpRequestHandler.kt | 25 ++++++++++++++--- .../src/org/jetbrains/io/netty.kt | 28 +++++++++++++++++++ 9 files changed, 72 insertions(+), 36 deletions(-) diff --git a/platform/built-in-server-api/src/org/jetbrains/ide/HttpRequestHandler.java b/platform/built-in-server-api/src/org/jetbrains/ide/HttpRequestHandler.java index 7ac77ba49554..18f473c77e8a 100644 --- a/platform/built-in-server-api/src/org/jetbrains/ide/HttpRequestHandler.java +++ b/platform/built-in-server-api/src/org/jetbrains/ide/HttpRequestHandler.java @@ -50,4 +50,8 @@ public abstract class HttpRequestHandler { */ public abstract boolean process(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException; + + public boolean isAllowRequestOnlyFromLocalOrigin() { + return true; + } } diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java index 1fa74d36be45..2789e1a00995 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java @@ -33,6 +33,7 @@ import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.ide.BuiltInServerManager; +import org.jetbrains.io.NettyKt; import java.util.Collections; import java.util.List; @@ -83,7 +84,7 @@ public class BuiltInWebBrowserUrlProvider extends WebBrowserUrlProvider implemen } String host = currentAuthority.substring(0, portIndex); - if (!BuiltInWebServerKt.isOwnHostName(host)) { + if (!NettyKt.isOwnHostName(host)) { return false; } diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index 1dd2f1b53e3c..8942e1d80f89 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -28,7 +28,6 @@ import com.intellij.util.UriUtil import com.intellij.util.directoryStreamIfExists import com.intellij.util.io.URLUtil import com.intellij.util.isDirectory -import com.intellij.util.net.NetUtils import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.FullHttpRequest import io.netty.handler.codec.http.HttpMethod @@ -36,9 +35,8 @@ import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.QueryStringDecoder import org.jetbrains.ide.HttpRequestHandler import org.jetbrains.io.host +import org.jetbrains.io.isOwnHostName import org.jetbrains.io.send -import java.net.InetAddress -import java.net.UnknownHostException import java.nio.file.Path internal val LOG = Logger.getInstance(BuiltInWebServer::class.java) @@ -219,25 +217,4 @@ fun findIndexFile(basedir: Path): Path? { } } return null -} - -fun isOwnHostName(host: String): Boolean { - if (NetUtils.isLocalhost(host)) { - return true - } - - try { - val address = InetAddress.getByName(host) - if (host == address.hostAddress || host.equals(address.canonicalHostName, ignoreCase = true)) { - return true - } - - val localHostName = InetAddress.getLocalHost().hostName - // WEB-8889 - // develar.local is own host name: develar. equals to "develar.labs.intellij.net" (canonical host name) - return localHostName.equals(host, ignoreCase = true) || (host.endsWith(".local") && localHostName.regionMatches(0, host, 0, host.length - ".local".length, true)) - } - catch (ignored: UnknownHostException) { - return false - } } \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/ide/DiffHttpService.java b/platform/built-in-server/src/org/jetbrains/ide/DiffHttpService.java index d3b76cf3b5c1..0652efa37ff5 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/DiffHttpService.java +++ b/platform/built-in-server/src/org/jetbrains/ide/DiffHttpService.java @@ -169,4 +169,9 @@ final class DiffHttpService extends RestService { reader.endArray(); return null; } + + @Override + public boolean isAllowRequestOnlyFromLocalOrigin() { + return false; + } } diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java index 1a6ddfc1f86b..d2a54ca323a2 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java +++ b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java @@ -347,4 +347,9 @@ class OpenFileHttpService extends RestService { public boolean focused = true; } + + @Override + public boolean isAllowRequestOnlyFromLocalOrigin() { + return false; + } } diff --git a/platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.java b/platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.java index 428635643535..70151e1962bc 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.java +++ b/platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.java @@ -74,4 +74,9 @@ public class ProjectSetRequestHandler extends RestService { sendOk(request, context); return null; } + + @Override + public boolean isAllowRequestOnlyFromLocalOrigin() { + return false; + } } diff --git a/platform/platform-api/src/com/intellij/util/net/NetUtils.java b/platform/platform-api/src/com/intellij/util/net/NetUtils.java index e00978ee0c6a..c261cea5d7ef 100644 --- a/platform/platform-api/src/com/intellij/util/net/NetUtils.java +++ b/platform/platform-api/src/com/intellij/util/net/NetUtils.java @@ -49,13 +49,7 @@ public class NetUtils { } public static InetAddress getLoopbackAddress() { - try { - // todo use JDK 7 InetAddress.getLoopbackAddress() - return InetAddress.getByName(null); - } - catch (UnknownHostException e) { - throw new RuntimeException(e); - } + return InetAddress.getLoopbackAddress(); } public static boolean isLocalhost(@NotNull String host) { diff --git a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt index bfad9825627f..c77a9b6e832e 100644 --- a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt +++ b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright 2000-2015 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.apache.sanselan.Sanselan import org.jetbrains.ide.HttpRequestHandler import java.awt.image.BufferedImage +import java.net.URI private val PREV_HANDLER = AttributeKey.valueOf("DelegatingHttpRequestHandler.handler") @@ -38,10 +39,27 @@ internal class DelegatingHttpRequestHandler : DelegatingHttpRequestHandlerBase() override fun process(context: ChannelHandlerContext, request: FullHttpRequest, urlDecoder: QueryStringDecoder): Boolean { + fun HttpRequestHandler.checkAndProcess(): Boolean { + if (isAllowRequestOnlyFromLocalOrigin) { + request.origin?.let { + try { + if (!isOwnHostName(URI(it).host)) { + return false + } + } + catch (e: Exception) { + return false + } + } + } + + return process(urlDecoder, request, context) + } + val prevHandlerAttribute = context.attr(PREV_HANDLER) val connectedHandler = prevHandlerAttribute.get() if (connectedHandler != null) { - if (connectedHandler.isSupported(request) && connectedHandler.process(urlDecoder, request, context)) { + if (connectedHandler.isSupported(request) && connectedHandler.checkAndProcess()) { return true } // prev cached connectedHandler is not suitable for this request, so, let's find it again @@ -50,7 +68,7 @@ internal class DelegatingHttpRequestHandler : DelegatingHttpRequestHandlerBase() for (handler in HttpRequestHandler.EP_NAME.extensions) { try { - if (handler.isSupported(request) && handler.process(urlDecoder, request, context)) { + if (handler.isSupported(request) && handler.checkAndProcess()) { prevHandlerAttribute.set(handler) return true } @@ -58,7 +76,6 @@ internal class DelegatingHttpRequestHandler : DelegatingHttpRequestHandlerBase() catch (e: Throwable) { Logger.getInstance(BuiltInServer::class.java).error(e) } - } if (urlDecoder.path() == "/favicon.ico") { diff --git a/platform/platform-impl/src/org/jetbrains/io/netty.kt b/platform/platform-impl/src/org/jetbrains/io/netty.kt index ceb922cc1a34..6ddf9d49b31e 100644 --- a/platform/platform-impl/src/org/jetbrains/io/netty.kt +++ b/platform/platform-impl/src/org/jetbrains/io/netty.kt @@ -17,6 +17,7 @@ package org.jetbrains.io import com.intellij.openapi.util.Condition import com.intellij.openapi.util.Conditions +import com.intellij.util.net.NetUtils import io.netty.bootstrap.Bootstrap import io.netty.bootstrap.ServerBootstrap import io.netty.buffer.ByteBuf @@ -32,7 +33,9 @@ import io.netty.handler.ssl.SslHandler import io.netty.util.concurrent.GenericFutureListener import org.jetbrains.concurrency.AsyncPromise import org.jetbrains.ide.PooledThreadExecutor +import java.net.InetAddress import java.net.InetSocketAddress +import java.net.UnknownHostException import java.util.concurrent.TimeUnit inline fun Bootstrap.handler(crossinline task: (Channel) -> Unit): Bootstrap { @@ -59,6 +62,7 @@ fun oioClientBootstrap(): Bootstrap { } inline fun ChannelFuture.addChannelListener(crossinline listener: (future: ChannelFuture) -> Unit) { + @Suppress("RedundantSamConstructor") addListener(GenericFutureListener { listener(it) }) } @@ -98,6 +102,9 @@ val Channel.uriScheme: String val HttpRequest.host: String? get() = headers().getAsString(HttpHeaderNames.HOST) +val HttpRequest.origin: String? + get() = headers().getAsString(HttpHeaderNames.ORIGIN) + inline fun ByteBuf.releaseIfError(task: () -> T): T { try { return task() @@ -110,4 +117,25 @@ inline fun ByteBuf.releaseIfError(task: () -> T): T { throw e } } +} + +fun isOwnHostName(host: String): Boolean { + if (NetUtils.isLocalhost(host)) { + return true + } + + try { + val address = InetAddress.getByName(host) + if (host == address.hostAddress || host.equals(address.canonicalHostName, ignoreCase = true)) { + return true + } + + val localHostName = InetAddress.getLocalHost().hostName + // WEB-8889 + // develar.local is own host name: develar. equals to "develar.labs.intellij.net" (canonical host name) + return localHostName.equals(host, ignoreCase = true) || (host.endsWith(".local") && localHostName.regionMatches(0, host, 0, host.length - ".local".length, true)) + } + catch (ignored: UnknownHostException) { + return false + } } \ No newline at end of file From b6762b192f71f65b1da33e04bfcf7f63c2ae20b5 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 11 Apr 2016 14:16:00 +0200 Subject: [PATCH 02/67] IDEA-CR-10038 check Referer as well --- .../io/DelegatingHttpRequestHandler.kt | 24 ++++++++++--------- .../src/org/jetbrains/io/netty.kt | 3 +++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt index c77a9b6e832e..e6d1d39eb6b0 100644 --- a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt +++ b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt @@ -40,17 +40,8 @@ internal class DelegatingHttpRequestHandler : DelegatingHttpRequestHandlerBase() request: FullHttpRequest, urlDecoder: QueryStringDecoder): Boolean { fun HttpRequestHandler.checkAndProcess(): Boolean { - if (isAllowRequestOnlyFromLocalOrigin) { - request.origin?.let { - try { - if (!isOwnHostName(URI(it).host)) { - return false - } - } - catch (e: Exception) { - return false - } - } + if (isAllowRequestOnlyFromLocalOrigin && (!parseAndCheckIsOwnHostName(request.origin) || !parseAndCheckIsOwnHostName(request.referrer))) { + return false } return process(urlDecoder, request, context) @@ -102,4 +93,15 @@ internal class DelegatingHttpRequestHandler : DelegatingHttpRequestHandlerBase() super.exceptionCaught(context, cause) } } +} + +private fun parseAndCheckIsOwnHostName(uri: String?): Boolean { + try { + if (uri == null || isOwnHostName(URI(uri).host)) { + return true + } + } + catch (ignored: Exception) { + } + return false } \ No newline at end of file diff --git a/platform/platform-impl/src/org/jetbrains/io/netty.kt b/platform/platform-impl/src/org/jetbrains/io/netty.kt index 6ddf9d49b31e..367a53806429 100644 --- a/platform/platform-impl/src/org/jetbrains/io/netty.kt +++ b/platform/platform-impl/src/org/jetbrains/io/netty.kt @@ -105,6 +105,9 @@ val HttpRequest.host: String? val HttpRequest.origin: String? get() = headers().getAsString(HttpHeaderNames.ORIGIN) +val HttpRequest.referrer: String? + get() = headers().getAsString(HttpHeaderNames.REFERER) + inline fun ByteBuf.releaseIfError(task: () -> T): T { try { return task() From 9480e5f00fe16b64f6f7254532063397d3eb0d49 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 11 Apr 2016 15:22:33 +0200 Subject: [PATCH 03/67] cleanup --- .../src/org/jetbrains/builtInWebServer/StaticFileHandler.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt index ea215e535709..6830ed6c9b16 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt @@ -1,7 +1,6 @@ package org.jetbrains.builtInWebServer import com.intellij.openapi.project.Project -import com.intellij.openapi.util.text.StringUtilRt import com.intellij.util.PathUtilRt import com.intellij.util.isDirectory import io.netty.buffer.ByteBufUtf8Writer @@ -24,10 +23,9 @@ private class StaticFileHandler : WebServerFileHandler() { override fun process(pathInfo: PathInfo, canonicalPath: CharSequence, project: Project, request: FullHttpRequest, channel: Channel, projectNameIfNotCustomHost: String?): Boolean { if (pathInfo.ioFile != null || pathInfo.file!!.isInLocalFileSystem) { val ioFile = pathInfo.ioFile ?: Paths.get(pathInfo.file!!.path) - - val nameSequence = pathInfo.name + val nameSequence = ioFile.fileName.toString() //noinspection SpellCheckingInspection - if (StringUtilRt.endsWithIgnoreCase(nameSequence, ".shtml") || StringUtilRt.endsWithIgnoreCase(nameSequence, ".stm") || StringUtilRt.endsWithIgnoreCase(nameSequence, ".shtm")) { + if (nameSequence.endsWith(".shtml", true) || nameSequence.endsWith(".stm", true) || nameSequence.endsWith(".shtm", true)) { processSsi(ioFile, PathUtilRt.getParentPath(canonicalPath.toString()), project, request, channel) return true } From 161378798234d70fd16e9d8304ffefd2abc5c1a7 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 11 Apr 2016 16:16:24 +0200 Subject: [PATCH 04/67] don't expose file status if not local origin --- .../src/org/jetbrains/ide/OpenFileHttpService.java | 4 +++- .../src/org/jetbrains/io/DelegatingHttpRequestHandler.kt | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java index d2a54ca323a2..dfc841949790 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java +++ b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java @@ -44,6 +44,7 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.builtInWebServer.WebServerPathToFileManager; import org.jetbrains.concurrency.AsyncPromise; import org.jetbrains.concurrency.Promise; +import org.jetbrains.io.DelegatingHttpRequestHandlerKt; import javax.swing.*; import java.io.File; @@ -144,7 +145,8 @@ class OpenFileHttpService extends RestService { @Override public void consume(Throwable throwable) { if (throwable == NOT_FOUND) { - sendStatus(HttpResponseStatus.NOT_FOUND, keepAlive, channel); + // don't expose file status if not local origin + sendStatus(DelegatingHttpRequestHandlerKt.isLocalOrigin(request) ? HttpResponseStatus.NOT_FOUND : HttpResponseStatus.OK, keepAlive, channel); } else { // todo send error diff --git a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt index e6d1d39eb6b0..1e2e75721c1e 100644 --- a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt +++ b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt @@ -23,6 +23,7 @@ import io.netty.buffer.Unpooled import io.netty.channel.ChannelHandler import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.FullHttpRequest +import io.netty.handler.codec.http.HttpRequest import io.netty.handler.codec.http.QueryStringDecoder import io.netty.util.AttributeKey import org.apache.sanselan.ImageFormat @@ -40,7 +41,7 @@ internal class DelegatingHttpRequestHandler : DelegatingHttpRequestHandlerBase() request: FullHttpRequest, urlDecoder: QueryStringDecoder): Boolean { fun HttpRequestHandler.checkAndProcess(): Boolean { - if (isAllowRequestOnlyFromLocalOrigin && (!parseAndCheckIsOwnHostName(request.origin) || !parseAndCheckIsOwnHostName(request.referrer))) { + if (isAllowRequestOnlyFromLocalOrigin && !request.isLocalOrigin()) { return false } @@ -95,6 +96,8 @@ internal class DelegatingHttpRequestHandler : DelegatingHttpRequestHandlerBase() } } +fun HttpRequest.isLocalOrigin() = parseAndCheckIsOwnHostName(origin) && parseAndCheckIsOwnHostName(referrer) + private fun parseAndCheckIsOwnHostName(uri: String?): Boolean { try { if (uri == null || isOwnHostName(URI(uri).host)) { From cfd55771e4e2a00ad45372c5b524f4f8a3d34161 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 11 Apr 2016 16:34:34 +0200 Subject: [PATCH 05/67] check intermediate directories --- .../builtInWebServer/StaticFileHandler.kt | 33 +++++++++++++++---- .../testSrc/BuiltInWebServerTest.kt | 23 +++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt index 6830ed6c9b16..6a0355247728 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt @@ -1,6 +1,7 @@ package org.jetbrains.builtInWebServer import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VFileProperty import com.intellij.util.PathUtilRt import com.intellij.util.isDirectory import io.netty.buffer.ByteBufUtf8Writer @@ -30,10 +31,15 @@ private class StaticFileHandler : WebServerFileHandler() { return true } - sendIoFile(channel, ioFile, request) + sendIoFile(channel, ioFile, Paths.get(pathInfo.root.path), request) } else { val file = pathInfo.file!! + if (file.`is`(VFileProperty.HIDDEN)) { + HttpResponseStatus.FORBIDDEN.send(channel, request) + return true + } + val response = FileResponses.prepareSend(request, channel, file.timeStamp, file.name) ?: return true val keepAlive = response.addKeepAliveIfNeed(request) @@ -92,14 +98,27 @@ private class StaticFileHandler : WebServerFileHandler() { } } -fun sendIoFile(channel: Channel, ioFile: Path, request: HttpRequest) { - if (hasAccess(ioFile)) { - FileResponses.sendFile(request, channel, ioFile) - } - else { +private fun sendIoFile(channel: Channel, file: Path, root: Path, request: HttpRequest) { + if (file.isDirectory()) { HttpResponseStatus.FORBIDDEN.send(channel, request) } + else if (checkAccess(channel, file, request, root)) { + FileResponses.sendFile(request, channel, file) + } +} + +fun checkAccess(channel: Channel, file: Path, request: HttpRequest, root: Path): Boolean { + var parent = file + do { + if (!hasAccess(parent)) { + HttpResponseStatus.FORBIDDEN.send(channel, request) + return false + } + parent = parent.parent ?: break + } + while (parent != root) + return true } // deny access to .htaccess files -private fun hasAccess(result: Path) = !result.isDirectory() && Files.isReadable(result) && !(Files.isHidden(result) || result.fileName.toString().startsWith(".ht")) \ No newline at end of file +private fun hasAccess(result: Path) = Files.isReadable(result) && !(Files.isHidden(result) || result.fileName.toString().startsWith(".ht")) \ No newline at end of file diff --git a/platform/built-in-server/testSrc/BuiltInWebServerTest.kt b/platform/built-in-server/testSrc/BuiltInWebServerTest.kt index 519cc7b4f440..d457fac331be 100644 --- a/platform/built-in-server/testSrc/BuiltInWebServerTest.kt +++ b/platform/built-in-server/testSrc/BuiltInWebServerTest.kt @@ -6,6 +6,7 @@ import com.intellij.openapi.module.EmptyModuleType import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ModuleRootModificationUtil +import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.LocalFileSystem @@ -16,6 +17,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.ClassRule import org.junit.Rule import org.junit.Test +import java.nio.file.Files internal class BuiltInWebServerTest : BuiltInServerTestCase() { override val urlPathPrefix: String @@ -89,4 +91,25 @@ internal class HeavyBuiltInWebServerTest { testUrl("http://localhost:${BuiltInServerManager.getInstance().port}/$webPath", HttpResponseStatus.NOT_FOUND) } } + + @Test + fun `hidden dir`() { + val projectDir = tempDirManager.newPath().resolve("foo/bar") + val projectDirPath = projectDir.systemIndependentPath + createHeavyProject("$projectDirPath/test.ipr").use { project -> + projectDir.createDirectories() + LocalFileSystem.getInstance().refreshAndFindFileByPath(projectDirPath) + createModule(projectDirPath, project) + + val dir = projectDir.resolve(".doNotExposeMe") + if (SystemInfo.isWindows) { + Files.setAttribute(dir, "dos:hidden", true) + } + + val path = dir.resolve("foo").write("doNotExposeMe").systemIndependentPath + val relativePath = FileUtil.getRelativePath(project.basePath!!, path, '/') + val webPath = StringUtil.replace(UrlEscapers.urlPathSegmentEscaper().escape("${project.name}/$relativePath"), "%2F", "/") + testUrl("http://localhost:${BuiltInServerManager.getInstance().port}/$webPath", HttpResponseStatus.FORBIDDEN) + } + } } \ No newline at end of file From 139ad5aa9269ca0d2a2e667e90409270d3a5dc37 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 11 Apr 2016 17:41:58 +0200 Subject: [PATCH 06/67] convert OpenFileHttpService to kotlin --- .../jetbrains/ide/OpenFileHttpService.java | 446 +++++++----------- 1 file changed, 171 insertions(+), 275 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java index dfc841949790..a027da1575ce 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java +++ b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java @@ -13,45 +13,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jetbrains.ide; +package org.jetbrains.ide -import com.intellij.openapi.application.AccessToken; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.application.ModalityState; -import com.intellij.openapi.application.WriteAction; -import com.intellij.openapi.fileEditor.OpenFileDescriptor; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectManager; -import com.intellij.openapi.project.ProjectUtil; -import com.intellij.openapi.util.Ref; -import com.intellij.openapi.util.io.FileUtil; -import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.util.text.StringUtilRt; -import com.intellij.openapi.vcs.ProjectLevelVcsManager; -import com.intellij.openapi.vcs.VcsRoot; -import com.intellij.openapi.vfs.LocalFileSystem; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.openapi.vfs.newvfs.ManagingFS; -import com.intellij.openapi.vfs.newvfs.RefreshQueue; -import com.intellij.openapi.vfs.newvfs.RefreshSession; -import com.intellij.ui.AppUIUtil; -import com.intellij.util.Consumer; -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.*; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.builtInWebServer.WebServerPathToFileManager; -import org.jetbrains.concurrency.AsyncPromise; -import org.jetbrains.concurrency.Promise; -import org.jetbrains.io.DelegatingHttpRequestHandlerKt; +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.project.ProjectUtil +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.util.text.StringUtilRt +import com.intellij.openapi.vcs.ProjectLevelVcsManager +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.newvfs.ManagingFS +import com.intellij.openapi.vfs.newvfs.RefreshQueue +import com.intellij.ui.AppUIUtil +import com.intellij.util.exists +import com.intellij.util.systemIndependentPath +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.http.* +import org.jetbrains.builtInWebServer.WebServerPathToFileManager +import org.jetbrains.concurrency.AsyncPromise +import org.jetbrains.concurrency.Promise +import org.jetbrains.concurrency.catchError +import org.jetbrains.concurrency.rejectedPromise +import org.jetbrains.io.isLocalOrigin +import java.nio.file.Path +import java.nio.file.Paths +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.regex.Pattern -import javax.swing.*; -import java.io.File; -import java.io.IOException; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +private val NOT_FOUND = Promise.createError("not found") +private val LINE_AND_COLUMN = Pattern.compile("^(.*?)(?::(\\d+))?(?::(\\d+))?$") /** * @api {get} /file Open file @@ -75,283 +71,183 @@ import java.util.regex.Pattern; * @apiExample {curl} Query parameters * curl http://localhost:63342/api/file?file=path/to/file.kt&line=100&column=34 */ -class OpenFileHttpService extends RestService { - private static final RuntimeException NOT_FOUND = Promise.createError("not found"); - private static final Pattern LINE_AND_COLUMN = Pattern.compile("^(.*?)(?::(\\d+))?(?::(\\d+))?$"); - private long lastTimeRejected = -1; - private long waitUntilNextRequestTimeout = 0; +internal class OpenFileHttpService : RestService() { + @Volatile private var refreshSessionId: Long = 0 + private val requests = ConcurrentLinkedQueue() - private volatile long refreshSessionId = 0; - private final ConcurrentLinkedQueue requests = new ConcurrentLinkedQueue(); + override fun getServiceName() = "file" - @NotNull - @Override - protected String getServiceName() { - return "file"; - } + override fun isMethodSupported(method: HttpMethod) = method === HttpMethod.GET || method === HttpMethod.POST - @Override - protected boolean isMethodSupported(@NotNull HttpMethod method) { - return method == HttpMethod.GET || method == HttpMethod.POST; - } + override fun execute(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): String? { + val keepAlive = HttpUtil.isKeepAlive(request) + val channel = context.channel() - @Nullable - @Override - public String execute(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException { - final boolean keepAlive = HttpUtil.isKeepAlive(request); - final Channel channel = context.channel(); - - OpenFileRequest apiRequest; - if (request.method() == HttpMethod.POST) { - apiRequest = gson.getValue().fromJson(createJsonReader(request), OpenFileRequest.class); + val apiRequest: OpenFileRequest + if (request.method() === HttpMethod.POST) { + apiRequest = gson.value.fromJson(RestService.createJsonReader(request), OpenFileRequest::class.java) } else { - apiRequest = new OpenFileRequest(); - apiRequest.file = StringUtil.nullize(getStringParameter("file", urlDecoder), true); - apiRequest.line = getIntParameter("line", urlDecoder); - apiRequest.column = getIntParameter("column", urlDecoder); - apiRequest.focused = getBooleanParameter("focused", urlDecoder, true); + apiRequest = OpenFileRequest() + apiRequest.file = StringUtil.nullize(RestService.getStringParameter("file", urlDecoder), true) + apiRequest.line = RestService.getIntParameter("line", urlDecoder) + apiRequest.column = RestService.getIntParameter("column", urlDecoder) + apiRequest.focused = RestService.getBooleanParameter("focused", urlDecoder, true) } - int prefixLength = 1 + PREFIX.length() + 1 + getServiceName().length() + 1; - String path = urlDecoder.path(); - if (path.length() > prefixLength) { - Matcher matcher = LINE_AND_COLUMN.matcher(path).region(prefixLength, path.length()); - LOG.assertTrue(matcher.matches()); + val prefixLength = 1 + RestService.PREFIX.length + 1 + serviceName.length + 1 + val path = urlDecoder.path() + if (path.length > prefixLength) { + val matcher = LINE_AND_COLUMN.matcher(path).region(prefixLength, path.length) + RestService.LOG.assertTrue(matcher.matches()) if (apiRequest.file == null) { - apiRequest.file = matcher.group(1).trim(); + apiRequest.file = matcher.group(1).trim { it <= ' ' } } if (apiRequest.line == -1) { - apiRequest.line = StringUtilRt.parseInt(matcher.group(2), 1); + apiRequest.line = StringUtilRt.parseInt(matcher.group(2), 1) } if (apiRequest.column == -1) { - apiRequest.column = StringUtilRt.parseInt(matcher.group(3), 1); + apiRequest.column = StringUtilRt.parseInt(matcher.group(3), 1) } } if (apiRequest.file == null) { - sendStatus(HttpResponseStatus.BAD_REQUEST, keepAlive, channel); - return null; + RestService.sendStatus(HttpResponseStatus.BAD_REQUEST, keepAlive, channel) + return null } - openFile(apiRequest) - .done(new Consumer() { - @Override - public void consume(Void aVoid) { - sendStatus(HttpResponseStatus.OK, keepAlive, channel); + openFile(apiRequest).done { RestService.sendStatus(HttpResponseStatus.OK, keepAlive, channel) } + .rejected { throwable -> + if (throwable === NOT_FOUND) { + // don't expose file status if not local origin + RestService.sendStatus(if (request.isLocalOrigin()) HttpResponseStatus.NOT_FOUND else HttpResponseStatus.OK, keepAlive, channel) } - }) - .rejected(new Consumer() { - @Override - public void consume(Throwable throwable) { - if (throwable == NOT_FOUND) { - // don't expose file status if not local origin - sendStatus(DelegatingHttpRequestHandlerKt.isLocalOrigin(request) ? HttpResponseStatus.NOT_FOUND : HttpResponseStatus.OK, keepAlive, channel); - } - else { - // todo send error - sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR, keepAlive, channel); - LOG.error(throwable); - } - } - }); - return null; - } - - private static void navigate(@Nullable Project project, @NotNull VirtualFile file, @NotNull OpenFileRequest request) { - if (project == null) { - project = getLastFocusedOrOpenedProject(); - if (project == null) { - project = ProjectManager.getInstance().getDefaultProject(); - } - } - - // OpenFileDescriptor line and column number are 0-based. - new OpenFileDescriptor(project, file, Math.max(request.line - 1, 0), Math.max(request.column - 1, 0)).navigate(true); - if (request.focused) { - com.intellij.ide.impl.ProjectUtil.focusProjectWindow(project, true); - } - } - - @NotNull - Promise openFile(@NotNull OpenFileRequest request) { - String systemIndependentName = FileUtil.toSystemIndependentName(FileUtil.expandUserHome(request.file)); - final File file = new File(systemIndependentName); - - if (file.isAbsolute()) { - if (com.intellij.ide.impl.ProjectUtil.isRemotePath(systemIndependentName)) { - final Ref> result = new Ref<>(); - try { - SwingUtilities.invokeAndWait(new Runnable() { - @Override - public void run() { - if (System.currentTimeMillis() - lastTimeRejected < waitUntilNextRequestTimeout) { - return; - } - boolean value = com.intellij.ide.impl.ProjectUtil - .confirmLoadingFromRemotePath(systemIndependentName, "warning.load.file.from.share", "title.load.file.from.share"); - - if (value != Boolean.TRUE) { - lastTimeRejected = System.currentTimeMillis(); - waitUntilNextRequestTimeout = Math.min(2 * Math.max(waitUntilNextRequestTimeout, 2000), 60 * 60 * 1000); //to avoid negative values - result.set(Promise.reject(NOT_FOUND)); - } else { - waitUntilNextRequestTimeout = 0; - } - } - }); - } catch (Throwable ignored) {} - - if (result.get() != null) { - return result.get(); + else { + // todo send error + RestService.sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR, keepAlive, channel) + RestService.LOG.error(throwable) } } + return null + } - return openAbsolutePath(file, request); + fun openFile(request: OpenFileRequest): Promise { + val path = FileUtil.expandUserHome(request.file!!) + val file = Paths.get(FileUtil.toSystemDependentName(path)) + if (file.isAbsolute) { + return openAbsolutePath(file, request) } // we don't want to call refresh for each attempt on findFileByRelativePath call, so, we do what ourSaveAndSyncHandlerImpl does on frame activation - RefreshQueue queue = RefreshQueue.getInstance(); - queue.cancelSession(refreshSessionId); - OpenFileTask task = new OpenFileTask(FileUtil.toCanonicalPath(systemIndependentName, '/'), request); - requests.offer(task); - RefreshSession session = queue.createSession(true, true, new Runnable() { - @Override - public void run() { - OpenFileTask task; - while ((task = requests.poll()) != null) { - try { - if (openRelativePath(task.path, task.request)) { - task.promise.setResult(null); - } - else { - task.promise.setError(NOT_FOUND); - } - } - catch (Throwable e) { - task.promise.setError(e); - } - } - } - }, ModalityState.NON_MODAL); - - session.addAllFiles(ManagingFS.getInstance().getLocalRoots()); - refreshSessionId = session.getId(); - session.launch(); - return task.promise; - } - - // path must be normalized - private static boolean openRelativePath(@NotNull final String path, @NotNull final OpenFileRequest request) { - VirtualFile virtualFile = null; - Project project = null; - - Project[] projects = ProjectManager.getInstance().getOpenProjects(); - for (Project openedProject : projects) { - VirtualFile openedProjectBaseDir = openedProject.getBaseDir(); - if (openedProjectBaseDir != null) { - virtualFile = openedProjectBaseDir.findFileByRelativePath(path); - } - - if (virtualFile == null) { - virtualFile = WebServerPathToFileManager.getInstance(openedProject).findVirtualFile(path); - } - if (virtualFile != null) { - project = openedProject; - break; - } - } - - if (virtualFile == null) { - for (Project openedProject : projects) { - for (VcsRoot vcsRoot : ProjectLevelVcsManager.getInstance(openedProject).getAllVcsRoots()) { - VirtualFile root = vcsRoot.getPath(); - if (root != null) { - virtualFile = root.findFileByRelativePath(path); - if (virtualFile != null) { - project = openedProject; - break; - } - } - } - } - } - - if (virtualFile == null) { - return false; - } - - final Project finalProject = project; - final VirtualFile finalVirtualFile = virtualFile; - AppUIUtil.invokeLaterIfProjectAlive(project, new Runnable() { - @Override - public void run() { - navigate(finalProject, finalVirtualFile, request); - } - }); - return true; - } - - @NotNull - private static Promise openAbsolutePath(@NotNull final File file, @NotNull final OpenFileRequest request) { - if (!file.exists()) { - return Promise.reject(NOT_FOUND); - } - - final AsyncPromise promise = new AsyncPromise(); - ApplicationManager.getApplication().invokeLater(new Runnable() { - @Override - public void run() { - try { - VirtualFile virtualFile; - AccessToken token = WriteAction.start(); - try { - virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file); - } - finally { - token.finish(); - } - - if (virtualFile == null) { - promise.setError(NOT_FOUND); + val queue = RefreshQueue.getInstance() + queue.cancelSession(refreshSessionId) + val mainTask = OpenFileTask(FileUtil.toCanonicalPath(FileUtil.toSystemIndependentName(path), '/'), request) + requests.offer(mainTask) + val session = queue.createSession(true, true, { + while (true) { + val task = requests.poll() ?: break + task.promise.catchError { + if (openRelativePath(task.path, task.request)) { + task.promise.setResult(null) } else { - navigate(ProjectUtil.guessProjectForContentFile(virtualFile), virtualFile, request); - promise.setResult(null); + task.promise.setError(NOT_FOUND) } } - catch (Throwable e) { - promise.setError(e); - } } - }); - return promise; + }, ModalityState.NON_MODAL) + + session.addAllFiles(*ManagingFS.getInstance().localRoots) + refreshSessionId = session.id + session.launch() + return mainTask.promise } - private static final class OpenFileTask { - final String path; - final OpenFileRequest request; + override fun isAllowRequestOnlyFromLocalOrigin() = false +} - final AsyncPromise promise = new AsyncPromise(); +internal class OpenFileRequest { + var file: String? = null + // The line number of the file (1-based) + var line = 0 + // The column number of the file (1-based) + var column = 0 - OpenFileTask(@NotNull String path, @NotNull OpenFileRequest request) { - this.path = path; - this.request = request; + var focused = true +} + +private class OpenFileTask(internal val path: String, internal val request: OpenFileRequest) { + internal val promise = AsyncPromise() +} + +private fun navigate(project: Project?, file: VirtualFile, request: OpenFileRequest) { + val effectiveProject = project ?: RestService.getLastFocusedOrOpenedProject() ?: ProjectManager.getInstance().defaultProject + // OpenFileDescriptor line and column number are 0-based. + OpenFileDescriptor(effectiveProject, file, Math.max(request.line - 1, 0), Math.max(request.column - 1, 0)).navigate(true) + if (request.focused) { + com.intellij.ide.impl.ProjectUtil.focusProjectWindow(project, true) + } +} + +// path must be normalized +private fun openRelativePath(path: String, request: OpenFileRequest): Boolean { + var virtualFile: VirtualFile? = null + var project: Project? = null + + val projects = ProjectManager.getInstance().openProjects + for (openedProject in projects) { + openedProject.baseDir?.let { + virtualFile = it.findFileByRelativePath(path) + } + + if (virtualFile == null) { + virtualFile = WebServerPathToFileManager.getInstance(openedProject).findVirtualFile(path) + } + if (virtualFile != null) { + project = openedProject + break } } - static final class OpenFileRequest { - public String file; - // The line number of the file (1-based) - public int line; - // The column number of the file (1-based) - public int column; - - public boolean focused = true; + if (virtualFile == null) { + for (openedProject in projects) { + for (vcsRoot in ProjectLevelVcsManager.getInstance(openedProject).allVcsRoots) { + val root = vcsRoot.path + if (root != null) { + virtualFile = root.findFileByRelativePath(path) + if (virtualFile != null) { + project = openedProject + break + } + } + } + } } - @Override - public boolean isAllowRequestOnlyFromLocalOrigin() { - return false; + virtualFile?.let { + AppUIUtil.invokeLaterIfProjectAlive(project!!, Runnable { navigate(project, it, request) }) + return true } + return false +} + +private fun openAbsolutePath(file: Path, request: OpenFileRequest): Promise { + if (!file.exists()) { + return rejectedPromise(NOT_FOUND) + } + + val promise = AsyncPromise() + ApplicationManager.getApplication().invokeLater { + promise.catchError { + val virtualFile = runWriteAction { LocalFileSystem.getInstance().refreshAndFindFileByPath(file.systemIndependentPath) } + if (virtualFile == null) { + promise.setError(NOT_FOUND) + } + else { + navigate(ProjectUtil.guessProjectForContentFile(virtualFile), virtualFile, request) + promise.setResult(null) + } + } + } + return promise } From 6b1b10bd3306612d480c17e4c808b68faa3ca9ed Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 11 Apr 2016 17:42:19 +0200 Subject: [PATCH 07/67] cleanup --- ...OpenFileHttpService.java => OpenFileHttpService.kt} | 0 .../src/org/jetbrains/ide/OpenFileXmlRpcHandler.java | 10 +++++----- 2 files changed, 5 insertions(+), 5 deletions(-) rename platform/built-in-server/src/org/jetbrains/ide/{OpenFileHttpService.java => OpenFileHttpService.kt} (100%) diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt similarity index 100% rename from platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java rename to platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java b/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java index 9487bdc3ea9a..83875b3a613a 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java +++ b/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java @@ -37,11 +37,11 @@ class OpenFileXmlRpcHandler { } private static boolean doOpen(@NotNull String path, int line, int column) { - OpenFileHttpService.OpenFileRequest request = new OpenFileHttpService.OpenFileRequest(); - request.file = path; - request.line = line; - request.column = column; - request.focused = false; + OpenFileRequest request = new OpenFileRequest(); + request.setFile(path); + request.setLine(line); + request.setColumn(column); + request.setFocused(false); return HttpRequestHandler.EP_NAME.findExtension(OpenFileHttpService.class).openFile(request).getState() != Promise.State.REJECTED; } } \ No newline at end of file From 9be60ac88207d1ee4d5bb36984902aa02047394a Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 11 Apr 2016 19:08:47 +0200 Subject: [PATCH 08/67] fix isOwnHostName and add tests --- .../BuiltInWebBrowserUrlProvider.java | 3 +- .../builtInWebServer/BuiltInWebServer.kt | 27 ++++++++++- .../testSrc/IsLocalHostTest.kt | 46 +++++++++++++++++++ .../io/DelegatingHttpRequestHandler.kt | 6 +-- .../src/org/jetbrains/io/netty.kt | 21 ++------- 5 files changed, 81 insertions(+), 22 deletions(-) create mode 100644 platform/built-in-server/testSrc/IsLocalHostTest.kt diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java index 2789e1a00995..1fa74d36be45 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java @@ -33,7 +33,6 @@ import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.ide.BuiltInServerManager; -import org.jetbrains.io.NettyKt; import java.util.Collections; import java.util.List; @@ -84,7 +83,7 @@ public class BuiltInWebBrowserUrlProvider extends WebBrowserUrlProvider implemen } String host = currentAuthority.substring(0, portIndex); - if (!NettyKt.isOwnHostName(host)) { + if (!BuiltInWebServerKt.isOwnHostName(host)) { return false; } diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index 8942e1d80f89..9cf61b372f9a 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -28,6 +28,7 @@ import com.intellij.util.UriUtil import com.intellij.util.directoryStreamIfExists import com.intellij.util.io.URLUtil import com.intellij.util.isDirectory +import com.intellij.util.net.NetUtils import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.FullHttpRequest import io.netty.handler.codec.http.HttpMethod @@ -35,8 +36,9 @@ import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.QueryStringDecoder import org.jetbrains.ide.HttpRequestHandler import org.jetbrains.io.host -import org.jetbrains.io.isOwnHostName import org.jetbrains.io.send +import java.io.IOException +import java.net.InetAddress import java.nio.file.Path internal val LOG = Logger.getInstance(BuiltInWebServer::class.java) @@ -217,4 +219,27 @@ fun findIndexFile(basedir: Path): Path? { } } return null +} + +// is host loopback/any or network interface address (i.e. not custom domain) +// must be not used to check is host on local machine +internal fun isOwnHostName(host: String): Boolean { + if (NetUtils.isLocalhost(host)) { + return true + } + + try { + val address = InetAddress.getByName(host) + if (host == address.hostAddress || host.equals(address.canonicalHostName, ignoreCase = true)) { + return true + } + + val localHostName = InetAddress.getLocalHost().hostName + // WEB-8889 + // develar.local is own host name: develar. equals to "develar.labs.intellij.net" (canonical host name) + return localHostName.equals(host, ignoreCase = true) || (host.endsWith(".local") && localHostName.regionMatches(0, host, 0, host.length - ".local".length, true)) + } + catch (ignored: IOException) { + return false + } } \ No newline at end of file diff --git a/platform/built-in-server/testSrc/IsLocalHostTest.kt b/platform/built-in-server/testSrc/IsLocalHostTest.kt new file mode 100644 index 000000000000..ee42797e5f53 --- /dev/null +++ b/platform/built-in-server/testSrc/IsLocalHostTest.kt @@ -0,0 +1,46 @@ +package org.jetbrains.io + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class IsLocalHostTest { + @Test + fun `google ip`() { + assertThat(isLocalHost("37.29.1.113")).isFalse() + } + + @Test + fun `google name`() { + assertThat(isLocalHost("google.com")).isFalse() + } + + @Test + fun `jetbrains name`() { + assertThat(isLocalHost("jetbrains.com")).isFalse() + } + + @Test + fun `unknown name`() { + assertThat(isLocalHost("foo.com")).isFalse() + } + + @Test + fun `unknown unqualified name`() { + assertThat(isLocalHost("local")).isFalse() + } + + @Test + fun `invalid ip`() { + assertThat(isLocalHost("0.0.0.0.0.0.0")).isFalse() + } + + @Test + fun `any`() { + assertThat(isLocalHost("0.0.0.0")).isTrue() + } + + @Test + fun `localhost`() { + assertThat(isLocalHost("localhost")).isTrue() + } +} \ No newline at end of file diff --git a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt index 1e2e75721c1e..c26fab75d7db 100644 --- a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt +++ b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt @@ -96,11 +96,11 @@ internal class DelegatingHttpRequestHandler : DelegatingHttpRequestHandlerBase() } } -fun HttpRequest.isLocalOrigin() = parseAndCheckIsOwnHostName(origin) && parseAndCheckIsOwnHostName(referrer) +fun HttpRequest.isLocalOrigin() = parseAndCheckIsLocalHost(origin) && parseAndCheckIsLocalHost(referrer) -private fun parseAndCheckIsOwnHostName(uri: String?): Boolean { +private fun parseAndCheckIsLocalHost(uri: String?): Boolean { try { - if (uri == null || isOwnHostName(URI(uri).host)) { + if (uri == null || isLocalHost(URI(uri).host)) { return true } } diff --git a/platform/platform-impl/src/org/jetbrains/io/netty.kt b/platform/platform-impl/src/org/jetbrains/io/netty.kt index 367a53806429..93b58a7af5de 100644 --- a/platform/platform-impl/src/org/jetbrains/io/netty.kt +++ b/platform/platform-impl/src/org/jetbrains/io/netty.kt @@ -17,7 +17,6 @@ package org.jetbrains.io import com.intellij.openapi.util.Condition import com.intellij.openapi.util.Conditions -import com.intellij.util.net.NetUtils import io.netty.bootstrap.Bootstrap import io.netty.bootstrap.ServerBootstrap import io.netty.buffer.ByteBuf @@ -33,9 +32,10 @@ import io.netty.handler.ssl.SslHandler import io.netty.util.concurrent.GenericFutureListener import org.jetbrains.concurrency.AsyncPromise import org.jetbrains.ide.PooledThreadExecutor +import java.io.IOException import java.net.InetAddress import java.net.InetSocketAddress -import java.net.UnknownHostException +import java.net.NetworkInterface import java.util.concurrent.TimeUnit inline fun Bootstrap.handler(crossinline task: (Channel) -> Unit): Bootstrap { @@ -122,23 +122,12 @@ inline fun ByteBuf.releaseIfError(task: () -> T): T { } } -fun isOwnHostName(host: String): Boolean { - if (NetUtils.isLocalhost(host)) { - return true - } - +fun isLocalHost(host: String): Boolean { try { val address = InetAddress.getByName(host) - if (host == address.hostAddress || host.equals(address.canonicalHostName, ignoreCase = true)) { - return true - } - - val localHostName = InetAddress.getLocalHost().hostName - // WEB-8889 - // develar.local is own host name: develar. equals to "develar.labs.intellij.net" (canonical host name) - return localHostName.equals(host, ignoreCase = true) || (host.endsWith(".local") && localHostName.regionMatches(0, host, 0, host.length - ".local".length, true)) + return address.isAnyLocalAddress || address.isLoopbackAddress || NetworkInterface.getByInetAddress(address) != null } - catch (ignored: UnknownHostException) { + catch (ignored: IOException) { return false } } \ No newline at end of file From fa00dfef3aa656fc28169d09a8986659908f0806 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Tue, 12 Apr 2016 12:28:33 +0200 Subject: [PATCH 09/67] =?UTF-8?q?OpenFileHttpService=20=E2=80=94=20do=20no?= =?UTF-8?q?t=20expose=20file=20status=20if=20not=20local=20origin=20and=20?= =?UTF-8?q?do=20not=20open=20hidden=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../builtInWebServer/StaticFileHandler.kt | 6 ++- .../org/jetbrains/ide/OpenFileHttpService.kt | 51 ++++++++++--------- .../jetbrains/ide/OpenFileXmlRpcHandler.java | 3 +- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt index 6a0355247728..ef779182b485 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt @@ -13,6 +13,7 @@ import org.jetbrains.builtInWebServer.ssi.SsiExternalResolver import org.jetbrains.builtInWebServer.ssi.SsiProcessor import org.jetbrains.io.FileResponses import org.jetbrains.io.addKeepAliveIfNeed +import org.jetbrains.io.isLocalOrigin import org.jetbrains.io.send import java.nio.file.Files import java.nio.file.Path @@ -107,11 +108,12 @@ private fun sendIoFile(channel: Channel, file: Path, root: Path, request: HttpRe } } -fun checkAccess(channel: Channel, file: Path, request: HttpRequest, root: Path): Boolean { +fun checkAccess(channel: Channel, file: Path, request: HttpRequest, root: Path = file.root, doNotExposeStatusIfNotLocalOrigin: Boolean = false): Boolean { var parent = file do { if (!hasAccess(parent)) { - HttpResponseStatus.FORBIDDEN.send(channel, request) + (if (doNotExposeStatusIfNotLocalOrigin && !request.isLocalOrigin()) HttpResponseStatus.OK else HttpResponseStatus.FORBIDDEN) + .send(channel, request) return false } parent = parent.parent ?: break diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt index a027da1575ce..3bf738f3a241 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt +++ b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt @@ -15,6 +15,7 @@ */ package org.jetbrains.ide +import com.intellij.ide.impl.ProjectUtil.focusProjectWindow import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.runWriteAction @@ -36,6 +37,7 @@ import com.intellij.util.systemIndependentPath import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.* import org.jetbrains.builtInWebServer.WebServerPathToFileManager +import org.jetbrains.builtInWebServer.checkAccess import org.jetbrains.concurrency.AsyncPromise import org.jetbrains.concurrency.Promise import org.jetbrains.concurrency.catchError @@ -85,21 +87,21 @@ internal class OpenFileHttpService : RestService() { val apiRequest: OpenFileRequest if (request.method() === HttpMethod.POST) { - apiRequest = gson.value.fromJson(RestService.createJsonReader(request), OpenFileRequest::class.java) + apiRequest = gson.value.fromJson(createJsonReader(request), OpenFileRequest::class.java) } else { apiRequest = OpenFileRequest() - apiRequest.file = StringUtil.nullize(RestService.getStringParameter("file", urlDecoder), true) - apiRequest.line = RestService.getIntParameter("line", urlDecoder) - apiRequest.column = RestService.getIntParameter("column", urlDecoder) - apiRequest.focused = RestService.getBooleanParameter("focused", urlDecoder, true) + apiRequest.file = StringUtil.nullize(getStringParameter("file", urlDecoder), true) + apiRequest.line = getIntParameter("line", urlDecoder) + apiRequest.column = getIntParameter("column", urlDecoder) + apiRequest.focused = getBooleanParameter("focused", urlDecoder, true) } - val prefixLength = 1 + RestService.PREFIX.length + 1 + serviceName.length + 1 + val prefixLength = 1 + PREFIX.length + 1 + serviceName.length + 1 val path = urlDecoder.path() if (path.length > prefixLength) { val matcher = LINE_AND_COLUMN.matcher(path).region(prefixLength, path.length) - RestService.LOG.assertTrue(matcher.matches()) + LOG.assertTrue(matcher.matches()) if (apiRequest.file == null) { apiRequest.file = matcher.group(1).trim { it <= ' ' } } @@ -112,30 +114,34 @@ internal class OpenFileHttpService : RestService() { } if (apiRequest.file == null) { - RestService.sendStatus(HttpResponseStatus.BAD_REQUEST, keepAlive, channel) + sendStatus(HttpResponseStatus.BAD_REQUEST, keepAlive, channel) return null } - openFile(apiRequest).done { RestService.sendStatus(HttpResponseStatus.OK, keepAlive, channel) } - .rejected { throwable -> - if (throwable === NOT_FOUND) { + val promise = openFile(apiRequest, context, request) ?: return null + promise.done { sendStatus(HttpResponseStatus.OK, keepAlive, channel) } + .rejected { + if (it === NOT_FOUND) { // don't expose file status if not local origin - RestService.sendStatus(if (request.isLocalOrigin()) HttpResponseStatus.NOT_FOUND else HttpResponseStatus.OK, keepAlive, channel) + sendStatus(if (request.isLocalOrigin()) HttpResponseStatus.NOT_FOUND else HttpResponseStatus.OK, keepAlive, channel) } else { // todo send error - RestService.sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR, keepAlive, channel) - RestService.LOG.error(throwable) + sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR, keepAlive, channel) + LOG.error(it) } } return null } - fun openFile(request: OpenFileRequest): Promise { + fun openFile(request: OpenFileRequest, context: ChannelHandlerContext?, httpRequest: HttpRequest?): Promise? { val path = FileUtil.expandUserHome(request.file!!) val file = Paths.get(FileUtil.toSystemDependentName(path)) if (file.isAbsolute) { - return openAbsolutePath(file, request) + if (!file.exists()) { + return rejectedPromise(NOT_FOUND) + } + return if (context == null || checkAccess(context.channel(), file, httpRequest!!, doNotExposeStatusIfNotLocalOrigin = true)) openAbsolutePath(file, request) else null } // we don't want to call refresh for each attempt on findFileByRelativePath call, so, we do what ourSaveAndSyncHandlerImpl does on frame activation @@ -185,7 +191,7 @@ private fun navigate(project: Project?, file: VirtualFile, request: OpenFileRequ // OpenFileDescriptor line and column number are 0-based. OpenFileDescriptor(effectiveProject, file, Math.max(request.line - 1, 0), Math.max(request.column - 1, 0)).navigate(true) if (request.focused) { - com.intellij.ide.impl.ProjectUtil.focusProjectWindow(project, true) + focusProjectWindow(project, true) } } @@ -224,18 +230,13 @@ private fun openRelativePath(path: String, request: OpenFileRequest): Boolean { } } - virtualFile?.let { + return virtualFile?.let { AppUIUtil.invokeLaterIfProjectAlive(project!!, Runnable { navigate(project, it, request) }) - return true - } - return false + true + } ?: false } private fun openAbsolutePath(file: Path, request: OpenFileRequest): Promise { - if (!file.exists()) { - return rejectedPromise(NOT_FOUND) - } - val promise = AsyncPromise() ApplicationManager.getApplication().invokeLater { promise.catchError { diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java b/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java index 83875b3a613a..410bf62eb9d8 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java +++ b/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java @@ -42,6 +42,7 @@ class OpenFileXmlRpcHandler { request.setLine(line); request.setColumn(column); request.setFocused(false); - return HttpRequestHandler.EP_NAME.findExtension(OpenFileHttpService.class).openFile(request).getState() != Promise.State.REJECTED; + Promise promise = HttpRequestHandler.EP_NAME.findExtension(OpenFileHttpService.class).openFile(request, null, null); + return promise != null && promise.getState() != Promise.State.REJECTED; } } \ No newline at end of file From c7f01496654526751505ef45cca2090e05800540 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Tue, 12 Apr 2016 16:55:59 +0200 Subject: [PATCH 10/67] trust our chrome-extensions --- .../jetbrains/io/DelegatingHttpRequestHandler.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt index c26fab75d7db..6de23f94ac5a 100644 --- a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt +++ b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt @@ -98,11 +98,18 @@ internal class DelegatingHttpRequestHandler : DelegatingHttpRequestHandlerBase() fun HttpRequest.isLocalOrigin() = parseAndCheckIsLocalHost(origin) && parseAndCheckIsLocalHost(referrer) +private fun isTrustedChromeExtension(uri: URI): Boolean { + return uri.scheme == "chrome-extension" && (uri.host == "hmhgeddbohgjknpmjagkdomcpobmllji" || uri.host == "offnedcbhjldheanlbojaefbfbllddna") +} + private fun parseAndCheckIsLocalHost(uri: String?): Boolean { + if (uri == null) { + return true + } + try { - if (uri == null || isLocalHost(URI(uri).host)) { - return true - } + val parsedUri = URI(uri) + return isTrustedChromeExtension(parsedUri) || isLocalHost(parsedUri.host) } catch (ignored: Exception) { } From b74204b174866d76d92f2129b57eee181ca34507 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Wed, 13 Apr 2016 07:47:37 +0200 Subject: [PATCH 11/67] move isLocalOrigin to netty.kt (as in 145 branch) --- .../io/DelegatingHttpRequestHandler.kt | 23 ------------------- .../src/org/jetbrains/io/netty.kt | 21 +++++++++++++++++ 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt index 6de23f94ac5a..99178d48b310 100644 --- a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt +++ b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt @@ -23,15 +23,12 @@ import io.netty.buffer.Unpooled import io.netty.channel.ChannelHandler import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.FullHttpRequest -import io.netty.handler.codec.http.HttpRequest import io.netty.handler.codec.http.QueryStringDecoder import io.netty.util.AttributeKey import org.apache.sanselan.ImageFormat import org.apache.sanselan.Sanselan import org.jetbrains.ide.HttpRequestHandler - import java.awt.image.BufferedImage -import java.net.URI private val PREV_HANDLER = AttributeKey.valueOf("DelegatingHttpRequestHandler.handler") @@ -94,24 +91,4 @@ internal class DelegatingHttpRequestHandler : DelegatingHttpRequestHandlerBase() super.exceptionCaught(context, cause) } } -} - -fun HttpRequest.isLocalOrigin() = parseAndCheckIsLocalHost(origin) && parseAndCheckIsLocalHost(referrer) - -private fun isTrustedChromeExtension(uri: URI): Boolean { - return uri.scheme == "chrome-extension" && (uri.host == "hmhgeddbohgjknpmjagkdomcpobmllji" || uri.host == "offnedcbhjldheanlbojaefbfbllddna") -} - -private fun parseAndCheckIsLocalHost(uri: String?): Boolean { - if (uri == null) { - return true - } - - try { - val parsedUri = URI(uri) - return isTrustedChromeExtension(parsedUri) || isLocalHost(parsedUri.host) - } - catch (ignored: Exception) { - } - return false } \ No newline at end of file diff --git a/platform/platform-impl/src/org/jetbrains/io/netty.kt b/platform/platform-impl/src/org/jetbrains/io/netty.kt index 93b58a7af5de..d9f355df42ec 100644 --- a/platform/platform-impl/src/org/jetbrains/io/netty.kt +++ b/platform/platform-impl/src/org/jetbrains/io/netty.kt @@ -36,6 +36,7 @@ import java.io.IOException import java.net.InetAddress import java.net.InetSocketAddress import java.net.NetworkInterface +import java.net.URI import java.util.concurrent.TimeUnit inline fun Bootstrap.handler(crossinline task: (Channel) -> Unit): Bootstrap { @@ -130,4 +131,24 @@ fun isLocalHost(host: String): Boolean { catch (ignored: IOException) { return false } +} + +fun HttpRequest.isLocalOrigin() = parseAndCheckIsLocalHost(origin) && parseAndCheckIsLocalHost(referrer) + +private fun isTrustedChromeExtension(uri: URI): Boolean { + return uri.scheme == "chrome-extension" && (uri.host == "hmhgeddbohgjknpmjagkdomcpobmllji" || uri.host == "offnedcbhjldheanlbojaefbfbllddna") +} + +private fun parseAndCheckIsLocalHost(uri: String?): Boolean { + if (uri == null) { + return true + } + + try { + val parsedUri = URI(uri) + return isTrustedChromeExtension(parsedUri) || isLocalHost(parsedUri.host) + } + catch (ignored: Exception) { + } + return false } \ No newline at end of file From 2c49382630c5d003cb7c4b6db4e2fa76514c466e Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Wed, 13 Apr 2016 09:37:21 +0200 Subject: [PATCH 12/67] =?UTF-8?q?HttpRequestHandler=20by=20default=20is=20?= =?UTF-8?q?accessible=20only=20from=20any=20or=20loopback=20IP=20address?= =?UTF-8?q?=20=E2=80=94=20domain=20name=20is=20not=20resolved=20and,=20so,?= =?UTF-8?q?=20forbidden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../builtInWebServer/BuiltInWebServer.kt | 8 +++---- .../org/jetbrains/ide/DiffHttpService.java | 5 +++-- .../org/jetbrains/ide/OpenFileHttpService.kt | 2 +- .../ide/ProjectSetRequestHandler.java | 5 +++-- .../testSrc/IsLocalHostTest.kt | 21 +++++++++++------- .../org/jetbrains/ide/HttpRequestHandler.java | 14 +++++++----- .../io/DelegatingHttpRequestHandler.kt | 10 +++------ .../src/org/jetbrains/io/netty.kt | 22 +++++++++++++++---- 8 files changed, 53 insertions(+), 34 deletions(-) rename platform/{built-in-server-api => platform-impl}/src/org/jetbrains/ide/HttpRequestHandler.java (89%) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index 9cf61b372f9a..604f2a6dce63 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -30,12 +30,10 @@ import com.intellij.util.io.URLUtil import com.intellij.util.isDirectory import com.intellij.util.net.NetUtils import io.netty.channel.ChannelHandlerContext -import io.netty.handler.codec.http.FullHttpRequest -import io.netty.handler.codec.http.HttpMethod -import io.netty.handler.codec.http.HttpResponseStatus -import io.netty.handler.codec.http.QueryStringDecoder +import io.netty.handler.codec.http.* import org.jetbrains.ide.HttpRequestHandler import org.jetbrains.io.host +import org.jetbrains.io.isLocalOrigin import org.jetbrains.io.send import java.io.IOException import java.net.InetAddress @@ -44,6 +42,8 @@ import java.nio.file.Path internal val LOG = Logger.getInstance(BuiltInWebServer::class.java) class BuiltInWebServer : HttpRequestHandler() { + override fun isAccessible(request: HttpRequest) = request.isLocalOrigin(false) + override fun isSupported(request: FullHttpRequest) = super.isSupported(request) || request.method() == HttpMethod.POST override fun process(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): Boolean { diff --git a/platform/built-in-server/src/org/jetbrains/ide/DiffHttpService.java b/platform/built-in-server/src/org/jetbrains/ide/DiffHttpService.java index 0652efa37ff5..061c32227831 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/DiffHttpService.java +++ b/platform/built-in-server/src/org/jetbrains/ide/DiffHttpService.java @@ -30,6 +30,7 @@ import com.intellij.openapi.util.text.StringUtil; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.QueryStringDecoder; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -171,7 +172,7 @@ final class DiffHttpService extends RestService { } @Override - public boolean isAllowRequestOnlyFromLocalOrigin() { - return false; + public boolean isAccessible(@NotNull HttpRequest request) { + return true; } } diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt index 3bf738f3a241..caff2214a43c 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt +++ b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt @@ -169,7 +169,7 @@ internal class OpenFileHttpService : RestService() { return mainTask.promise } - override fun isAllowRequestOnlyFromLocalOrigin() = false + override fun isAccessible(request: HttpRequest) = true } internal class OpenFileRequest { diff --git a/platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.java b/platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.java index 70151e1962bc..da22bb803530 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.java +++ b/platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.java @@ -23,6 +23,7 @@ import com.intellij.platform.ProjectSetReader; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.QueryStringDecoder; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -76,7 +77,7 @@ public class ProjectSetRequestHandler extends RestService { } @Override - public boolean isAllowRequestOnlyFromLocalOrigin() { - return false; + public boolean isAccessible(@NotNull HttpRequest request) { + return true; } } diff --git a/platform/built-in-server/testSrc/IsLocalHostTest.kt b/platform/built-in-server/testSrc/IsLocalHostTest.kt index ee42797e5f53..722138936003 100644 --- a/platform/built-in-server/testSrc/IsLocalHostTest.kt +++ b/platform/built-in-server/testSrc/IsLocalHostTest.kt @@ -6,41 +6,46 @@ import org.junit.Test class IsLocalHostTest { @Test fun `google ip`() { - assertThat(isLocalHost("37.29.1.113")).isFalse() + assertThat(isLocalHost("37.29.1.113", false)).isFalse() } @Test fun `google name`() { - assertThat(isLocalHost("google.com")).isFalse() + assertThat(isLocalHost("google.com", false)).isFalse() } @Test fun `jetbrains name`() { - assertThat(isLocalHost("jetbrains.com")).isFalse() + assertThat(isLocalHost("jetbrains.com", false)).isFalse() } @Test fun `unknown name`() { - assertThat(isLocalHost("foo.com")).isFalse() + assertThat(isLocalHost("foo.com", false)).isFalse() } @Test fun `unknown unqualified name`() { - assertThat(isLocalHost("local")).isFalse() + assertThat(isLocalHost("local", false)).isFalse() } @Test fun `invalid ip`() { - assertThat(isLocalHost("0.0.0.0.0.0.0")).isFalse() + assertThat(isLocalHost("0.0.0.0.0.0.0", false)).isFalse() } @Test fun `any`() { - assertThat(isLocalHost("0.0.0.0")).isTrue() + assertThat(isLocalHost("0.0.0.0", false)).isTrue() } @Test fun `localhost`() { - assertThat(isLocalHost("localhost")).isTrue() + assertThat(isLocalHost("localhost", false)).isTrue() + } + + @Test + fun `localhost only loopback`() { + assertThat(isLocalHost("localhost", true)).isTrue() } } \ No newline at end of file diff --git a/platform/built-in-server-api/src/org/jetbrains/ide/HttpRequestHandler.java b/platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java similarity index 89% rename from platform/built-in-server-api/src/org/jetbrains/ide/HttpRequestHandler.java rename to platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java index 18f473c77e8a..23730cbad529 100644 --- a/platform/built-in-server-api/src/org/jetbrains/ide/HttpRequestHandler.java +++ b/platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2015 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,10 @@ import com.intellij.openapi.extensions.ExtensionPointName; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.QueryStringDecoder; import org.jetbrains.annotations.NotNull; +import org.jetbrains.io.NettyKt; import java.io.IOException; @@ -41,6 +43,10 @@ public abstract class HttpRequestHandler { return false; } + public boolean isAccessible(@NotNull HttpRequest request) { + return NettyKt.isLocalOrigin(request); + } + public boolean isSupported(@NotNull FullHttpRequest request) { return request.method() == HttpMethod.GET || request.method() == HttpMethod.HEAD; } @@ -50,8 +56,4 @@ public abstract class HttpRequestHandler { */ public abstract boolean process(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException; - - public boolean isAllowRequestOnlyFromLocalOrigin() { - return true; - } -} +} \ No newline at end of file diff --git a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt index 99178d48b310..b500c0294ab5 100644 --- a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt +++ b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt @@ -38,17 +38,13 @@ internal class DelegatingHttpRequestHandler : DelegatingHttpRequestHandlerBase() request: FullHttpRequest, urlDecoder: QueryStringDecoder): Boolean { fun HttpRequestHandler.checkAndProcess(): Boolean { - if (isAllowRequestOnlyFromLocalOrigin && !request.isLocalOrigin()) { - return false - } - - return process(urlDecoder, request, context) + return isSupported(request) && isAccessible(request) && process(urlDecoder, request, context) } val prevHandlerAttribute = context.attr(PREV_HANDLER) val connectedHandler = prevHandlerAttribute.get() if (connectedHandler != null) { - if (connectedHandler.isSupported(request) && connectedHandler.checkAndProcess()) { + if (connectedHandler.checkAndProcess()) { return true } // prev cached connectedHandler is not suitable for this request, so, let's find it again @@ -57,7 +53,7 @@ internal class DelegatingHttpRequestHandler : DelegatingHttpRequestHandlerBase() for (handler in HttpRequestHandler.EP_NAME.extensions) { try { - if (handler.isSupported(request) && handler.checkAndProcess()) { + if (handler.checkAndProcess()) { prevHandlerAttribute.set(handler) return true } diff --git a/platform/platform-impl/src/org/jetbrains/io/netty.kt b/platform/platform-impl/src/org/jetbrains/io/netty.kt index d9f355df42ec..c46eceb88541 100644 --- a/platform/platform-impl/src/org/jetbrains/io/netty.kt +++ b/platform/platform-impl/src/org/jetbrains/io/netty.kt @@ -15,8 +15,10 @@ */ package org.jetbrains.io +import com.google.common.net.InetAddresses import com.intellij.openapi.util.Condition import com.intellij.openapi.util.Conditions +import com.intellij.util.net.NetUtils import io.netty.bootstrap.Bootstrap import io.netty.bootstrap.ServerBootstrap import io.netty.buffer.ByteBuf @@ -123,7 +125,18 @@ inline fun ByteBuf.releaseIfError(task: () -> T): T { } } -fun isLocalHost(host: String): Boolean { +fun isLocalHost(host: String, onlyAnyOrLoopback: Boolean): Boolean { + if (onlyAnyOrLoopback) { + if (NetUtils.isLocalhost(host)) { + return true + } + + if (!InetAddresses.isInetAddress(host)) { + return false + } + // if IP address, it is safe to use getByName (not affected by DNS rebinding) + } + try { val address = InetAddress.getByName(host) return address.isAnyLocalAddress || address.isLoopbackAddress || NetworkInterface.getByInetAddress(address) != null @@ -133,20 +146,21 @@ fun isLocalHost(host: String): Boolean { } } -fun HttpRequest.isLocalOrigin() = parseAndCheckIsLocalHost(origin) && parseAndCheckIsLocalHost(referrer) +@JvmOverloads +fun HttpRequest.isLocalOrigin(onlyAnyOrLoopback: Boolean = true) = parseAndCheckIsLocalHost(origin, onlyAnyOrLoopback) && parseAndCheckIsLocalHost(referrer, onlyAnyOrLoopback) private fun isTrustedChromeExtension(uri: URI): Boolean { return uri.scheme == "chrome-extension" && (uri.host == "hmhgeddbohgjknpmjagkdomcpobmllji" || uri.host == "offnedcbhjldheanlbojaefbfbllddna") } -private fun parseAndCheckIsLocalHost(uri: String?): Boolean { +private fun parseAndCheckIsLocalHost(uri: String?, onlyAnyOrLoopback: Boolean): Boolean { if (uri == null) { return true } try { val parsedUri = URI(uri) - return isTrustedChromeExtension(parsedUri) || isLocalHost(parsedUri.host) + return isTrustedChromeExtension(parsedUri) || isLocalHost(parsedUri.host, onlyAnyOrLoopback) } catch (ignored: Exception) { } From 63bf42b6abc2544d37800157a7c8583607ec9c0a Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Wed, 13 Apr 2016 10:30:49 +0200 Subject: [PATCH 13/67] check Host --- .../src/org/jetbrains/ide/HttpRequestHandler.java | 6 +++++- platform/platform-impl/src/org/jetbrains/io/netty.kt | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java b/platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java index 23730cbad529..0f03b280d8c3 100644 --- a/platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java +++ b/platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java @@ -43,8 +43,12 @@ public abstract class HttpRequestHandler { return false; } + @SuppressWarnings("SpellCheckingInspection") public boolean isAccessible(@NotNull HttpRequest request) { - return NettyKt.isLocalOrigin(request); + String host = NettyKt.getHost(request); + // If attacker.com DNS rebound to 127.0.0.1 and user open site directly — no Origin or Referer headers. + // So we should check Host header. + return host != null && NettyKt.isLocalOrigin(request) && NettyKt.parseAndCheckIsLocalHost(host); } public boolean isSupported(@NotNull FullHttpRequest request) { diff --git a/platform/platform-impl/src/org/jetbrains/io/netty.kt b/platform/platform-impl/src/org/jetbrains/io/netty.kt index c46eceb88541..336205bd7cb8 100644 --- a/platform/platform-impl/src/org/jetbrains/io/netty.kt +++ b/platform/platform-impl/src/org/jetbrains/io/netty.kt @@ -153,7 +153,8 @@ private fun isTrustedChromeExtension(uri: URI): Boolean { return uri.scheme == "chrome-extension" && (uri.host == "hmhgeddbohgjknpmjagkdomcpobmllji" || uri.host == "offnedcbhjldheanlbojaefbfbllddna") } -private fun parseAndCheckIsLocalHost(uri: String?, onlyAnyOrLoopback: Boolean): Boolean { +@JvmOverloads +fun parseAndCheckIsLocalHost(uri: String?, onlyAnyOrLoopback: Boolean = true): Boolean { if (uri == null) { return true } From a2340f52ec8b6b951b389096f83ff92da4d26b9d Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Wed, 13 Apr 2016 11:37:31 +0200 Subject: [PATCH 14/67] use InetAddress.getLoopbackAddress() --- .../src/com/intellij/compiler/server/BuildManager.java | 5 +++-- .../com/intellij/execution/runners/ProcessProxyImpl.java | 6 +++--- .../platform-api/src/com/intellij/util/net/NetUtils.java | 9 ++------- .../platform-impl/src/com/intellij/idea/SocketLock.java | 6 +++--- .../io/socketConnection/impl/SocketConnectionImpl.java | 7 +++---- .../src/org/jetbrains/io/BuiltInServer.java | 3 +-- .../intellij/lang/xpath/xslt/run/OutputTabAdapter.java | 4 ++-- 7 files changed, 17 insertions(+), 23 deletions(-) diff --git a/java/compiler/impl/src/com/intellij/compiler/server/BuildManager.java b/java/compiler/impl/src/com/intellij/compiler/server/BuildManager.java index 2d9742863629..3983bf2b36ab 100644 --- a/java/compiler/impl/src/com/intellij/compiler/server/BuildManager.java +++ b/java/compiler/impl/src/com/intellij/compiler/server/BuildManager.java @@ -112,6 +112,7 @@ import java.awt.*; import java.io.File; import java.io.FileFilter; import java.io.IOException; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.charset.Charset; import java.text.SimpleDateFormat; @@ -1205,7 +1206,7 @@ public class BuildManager implements Disposable { catch (Throwable e) { LOG.error(e); } - + final OSProcessHandler processHandler = new OSProcessHandler(cmdLine) { @Override protected boolean shouldDestroyProcessRecursively() { @@ -1336,7 +1337,7 @@ public class BuildManager implements Disposable { myMessageDispatcher); } }); - Channel serverChannel = bootstrap.bind(NetUtils.getLoopbackAddress(), 0).syncUninterruptibly().channel(); + Channel serverChannel = bootstrap.bind(InetAddress.getLoopbackAddress(), 0).syncUninterruptibly().channel(); myChannelRegistrar.add(serverChannel); return ((InetSocketAddress)serverChannel.localAddress()).getPort(); } diff --git a/java/execution/impl/src/com/intellij/execution/runners/ProcessProxyImpl.java b/java/execution/impl/src/com/intellij/execution/runners/ProcessProxyImpl.java index 525966359763..f09d7b553639 100644 --- a/java/execution/impl/src/com/intellij/execution/runners/ProcessProxyImpl.java +++ b/java/execution/impl/src/com/intellij/execution/runners/ProcessProxyImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2011 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,13 @@ package com.intellij.execution.runners; import com.intellij.execution.process.ProcessHandler; import com.intellij.openapi.util.Key; -import com.intellij.util.net.NetUtils; import org.jetbrains.annotations.NonNls; import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; +import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; @@ -95,7 +95,7 @@ class ProcessProxyImpl implements ProcessProxy { if (myWriter == null) { try { if (mySocket == null) { - mySocket = new Socket(NetUtils.getLoopbackAddress(), myPortNumber); + mySocket = new Socket(InetAddress.getLoopbackAddress(), myPortNumber); } myWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(mySocket.getOutputStream()))); } diff --git a/platform/platform-api/src/com/intellij/util/net/NetUtils.java b/platform/platform-api/src/com/intellij/util/net/NetUtils.java index c261cea5d7ef..f62c0f750d91 100644 --- a/platform/platform-api/src/com/intellij/util/net/NetUtils.java +++ b/platform/platform-api/src/com/intellij/util/net/NetUtils.java @@ -33,12 +33,6 @@ public class NetUtils { private NetUtils() { } - /** @deprecated use {@link #canConnectToSocket(String, int)} (to be remove in IDEA 17) */ - @SuppressWarnings("unused") - public static boolean canConnectToSocketOpenedByJavaProcess(String host, int port) { - return canConnectToSocket(host, port); - } - public static boolean canConnectToSocket(String host, int port) { if (isLocalhost(host)) { return !canBindToLocalSocket(host, port); @@ -48,6 +42,7 @@ public class NetUtils { } } + @Deprecated public static InetAddress getLoopbackAddress() { return InetAddress.getLoopbackAddress(); } @@ -218,6 +213,6 @@ public class NetUtils { } public static boolean isSniEnabled() { - return SystemInfo.isJavaVersionAtLeast("1.7") && SystemProperties.getBooleanProperty("jsse.enableSNIExtension", true); + return SystemProperties.getBooleanProperty("jsse.enableSNIExtension", true); } } diff --git a/platform/platform-impl/src/com/intellij/idea/SocketLock.java b/platform/platform-impl/src/com/intellij/idea/SocketLock.java index 0e4585b9f5f3..783e761c1069 100644 --- a/platform/platform-impl/src/com/intellij/idea/SocketLock.java +++ b/platform/platform-impl/src/com/intellij/idea/SocketLock.java @@ -27,7 +27,6 @@ import com.intellij.util.Consumer; import com.intellij.util.NotNullProducer; import com.intellij.util.PlatformUtils; import com.intellij.util.containers.MultiMap; -import com.intellij.util.net.NetUtils; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufOutputStream; import io.netty.channel.ChannelHandler; @@ -40,6 +39,7 @@ import org.jetbrains.io.MessageDecoder; import java.io.*; import java.lang.management.ManagementFactory; import java.net.ConnectException; +import java.net.InetAddress; import java.net.Socket; import java.util.Collection; import java.util.List; @@ -189,7 +189,7 @@ public final class SocketLock { log("trying: port=%s", portNumber); args = checkForJetBrainsProtocolCommand(args); try { - Socket socket = new Socket(NetUtils.getLoopbackAddress(), portNumber); + Socket socket = new Socket(InetAddress.getLoopbackAddress(), portNumber); try { socket.setSoTimeout(1000); @@ -249,7 +249,7 @@ public final class SocketLock { private static void printPID(int port) { try { - Socket socket = new Socket(NetUtils.getLoopbackAddress(), port); + Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); socket.setSoTimeout(1000); @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") DataOutputStream out = new DataOutputStream(socket.getOutputStream()); out.writeUTF(PID_COMMAND); diff --git a/platform/platform-impl/src/com/intellij/util/io/socketConnection/impl/SocketConnectionImpl.java b/platform/platform-impl/src/com/intellij/util/io/socketConnection/impl/SocketConnectionImpl.java index cfe3db1224a6..5fc8fa284a29 100644 --- a/platform/platform-impl/src/com/intellij/util/io/socketConnection/impl/SocketConnectionImpl.java +++ b/platform/platform-impl/src/com/intellij/util/io/socketConnection/impl/SocketConnectionImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2010 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ package com.intellij.util.io.socketConnection.impl; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.util.io.socketConnection.*; -import com.intellij.util.net.NetUtils; import org.jetbrains.annotations.NotNull; import java.io.IOException; @@ -72,7 +71,7 @@ public class SocketConnectionImpl Date: Wed, 13 Apr 2016 14:17:09 +0200 Subject: [PATCH 15/67] dns rebinging fix for built-in web server --- .../netty/io/netty/resolver/annotations.xml | 5 +++++ .../builtInWebServer/BuiltInWebServer.kt | 2 +- .../src/org/jetbrains/io/netty.kt | 22 ++++++++++++++----- 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 lib/annotations/netty/io/netty/resolver/annotations.xml diff --git a/lib/annotations/netty/io/netty/resolver/annotations.xml b/lib/annotations/netty/io/netty/resolver/annotations.xml new file mode 100644 index 000000000000..10089671fab8 --- /dev/null +++ b/lib/annotations/netty/io/netty/resolver/annotations.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index 604f2a6dce63..2e2c7395c70a 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -42,7 +42,7 @@ import java.nio.file.Path internal val LOG = Logger.getInstance(BuiltInWebServer::class.java) class BuiltInWebServer : HttpRequestHandler() { - override fun isAccessible(request: HttpRequest) = request.isLocalOrigin(false) + override fun isAccessible(request: HttpRequest) = request.isLocalOrigin(onlyAnyOrLoopback = false, hostsOnly = true) override fun isSupported(request: FullHttpRequest) = super.isSupported(request) || request.method() == HttpMethod.POST diff --git a/platform/platform-impl/src/org/jetbrains/io/netty.kt b/platform/platform-impl/src/org/jetbrains/io/netty.kt index 336205bd7cb8..9b88f0d50d7f 100644 --- a/platform/platform-impl/src/org/jetbrains/io/netty.kt +++ b/platform/platform-impl/src/org/jetbrains/io/netty.kt @@ -31,6 +31,7 @@ import io.netty.channel.socket.oio.OioSocketChannel import io.netty.handler.codec.http.HttpHeaderNames import io.netty.handler.codec.http.HttpRequest import io.netty.handler.ssl.SslHandler +import io.netty.resolver.HostsFileEntriesResolver import io.netty.util.concurrent.GenericFutureListener import org.jetbrains.concurrency.AsyncPromise import org.jetbrains.ide.PooledThreadExecutor @@ -125,7 +126,7 @@ inline fun ByteBuf.releaseIfError(task: () -> T): T { } } -fun isLocalHost(host: String, onlyAnyOrLoopback: Boolean): Boolean { +fun isLocalHost(host: String, onlyAnyOrLoopback: Boolean, hostsOnly: Boolean = false): Boolean { if (onlyAnyOrLoopback) { if (NetUtils.isLocalhost(host)) { return true @@ -137,9 +138,20 @@ fun isLocalHost(host: String, onlyAnyOrLoopback: Boolean): Boolean { // if IP address, it is safe to use getByName (not affected by DNS rebinding) } + fun InetAddress.isLocal() = isAnyLocalAddress || isLoopbackAddress || NetworkInterface.getByInetAddress(this) != null + try { val address = InetAddress.getByName(host) - return address.isAnyLocalAddress || address.isLoopbackAddress || NetworkInterface.getByInetAddress(address) != null + if (!address.isLocal()) { + return false + } + // hosts can contain remote addresses, so, we check it + if (hostsOnly && !InetAddresses.isInetAddress(host)) { + return HostsFileEntriesResolver.DEFAULT.address(host).let { it != null && it.isLocal() } + } + else { + return true + } } catch (ignored: IOException) { return false @@ -147,21 +159,21 @@ fun isLocalHost(host: String, onlyAnyOrLoopback: Boolean): Boolean { } @JvmOverloads -fun HttpRequest.isLocalOrigin(onlyAnyOrLoopback: Boolean = true) = parseAndCheckIsLocalHost(origin, onlyAnyOrLoopback) && parseAndCheckIsLocalHost(referrer, onlyAnyOrLoopback) +fun HttpRequest.isLocalOrigin(onlyAnyOrLoopback: Boolean = true, hostsOnly: Boolean = false) = parseAndCheckIsLocalHost(origin, onlyAnyOrLoopback, hostsOnly) && parseAndCheckIsLocalHost(referrer, onlyAnyOrLoopback, hostsOnly) private fun isTrustedChromeExtension(uri: URI): Boolean { return uri.scheme == "chrome-extension" && (uri.host == "hmhgeddbohgjknpmjagkdomcpobmllji" || uri.host == "offnedcbhjldheanlbojaefbfbllddna") } @JvmOverloads -fun parseAndCheckIsLocalHost(uri: String?, onlyAnyOrLoopback: Boolean = true): Boolean { +fun parseAndCheckIsLocalHost(uri: String?, onlyAnyOrLoopback: Boolean = true, hostsOnly: Boolean = false): Boolean { if (uri == null) { return true } try { val parsedUri = URI(uri) - return isTrustedChromeExtension(parsedUri) || isLocalHost(parsedUri.host, onlyAnyOrLoopback) + return isTrustedChromeExtension(parsedUri) || isLocalHost(parsedUri.host, onlyAnyOrLoopback, hostsOnly) } catch (ignored: Exception) { } From 9f6d9518044a77a4a8f608da0beace4d910858a2 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Wed, 13 Apr 2016 15:06:32 +0200 Subject: [PATCH 16/67] close connection if message > 8192 --- platform/platform-impl/src/com/intellij/idea/SocketLock.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/platform/platform-impl/src/com/intellij/idea/SocketLock.java b/platform/platform-impl/src/com/intellij/idea/SocketLock.java index 783e761c1069..0fcf78556d4f 100644 --- a/platform/platform-impl/src/com/intellij/idea/SocketLock.java +++ b/platform/platform-impl/src/com/intellij/idea/SocketLock.java @@ -325,6 +325,10 @@ public final class SocketLock { } contentLength = buffer.readUnsignedShort(); + if (contentLength > 8192) { + context.close(); + return; + } myState = State.CONTENT; } break; From b83ef506cd5bb599c74fdb130cbe5a65a7b4d5f3 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Wed, 13 Apr 2016 16:24:14 +0200 Subject: [PATCH 17/67] host doesn't contain scheme --- .../platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java b/platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java index 0f03b280d8c3..5166e95ba8fd 100644 --- a/platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java +++ b/platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java @@ -48,7 +48,7 @@ public abstract class HttpRequestHandler { String host = NettyKt.getHost(request); // If attacker.com DNS rebound to 127.0.0.1 and user open site directly — no Origin or Referer headers. // So we should check Host header. - return host != null && NettyKt.isLocalOrigin(request) && NettyKt.parseAndCheckIsLocalHost(host); + return host != null && NettyKt.isLocalOrigin(request) && NettyKt.parseAndCheckIsLocalHost("http://" + host); } public boolean isSupported(@NotNull FullHttpRequest request) { From ee2f9950478ad28bf06134f0dfb411f9441902b1 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Wed, 13 Apr 2016 18:05:39 +0200 Subject: [PATCH 18/67] cleanup --- .../platform-impl/src/org/jetbrains/io/NettyUtil.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/platform/platform-impl/src/org/jetbrains/io/NettyUtil.java b/platform/platform-impl/src/org/jetbrains/io/NettyUtil.java index b3541f95b383..8bb15db8e996 100644 --- a/platform/platform-impl/src/org/jetbrains/io/NettyUtil.java +++ b/platform/platform-impl/src/org/jetbrains/io/NettyUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2015 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpRequestDecoder; import io.netty.handler.codec.http.HttpResponseEncoder; import io.netty.handler.codec.http.cors.CorsConfig; +import io.netty.handler.codec.http.cors.CorsConfigBuilder; import io.netty.handler.codec.http.cors.CorsHandler; import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.util.concurrent.GlobalEventExecutor; @@ -234,8 +235,9 @@ public final class NettyUtil { if (pipeline.get(ChunkedWriteHandler.class) == null) { pipeline.addLast("chunkedWriteHandler", new ChunkedWriteHandler()); } - pipeline.addLast("corsHandler", new CorsHandlerDoNotUseOwnLogger(CorsConfig - .withAnyOrigin() + pipeline.addLast("corsHandler", new CorsHandlerDoNotUseOwnLogger(CorsConfigBuilder + .forAnyOrigin() + .shortCircuit() .allowCredentials() .allowNullOrigin() .allowedRequestMethods(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE, HttpMethod.HEAD, HttpMethod.PATCH) From 2ddce3be1b80d09e40f646af6b19b1324b9a49b9 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Thu, 14 Apr 2016 08:16:56 +0200 Subject: [PATCH 19/67] forbid POST requests from browser without Origin --- .../src/org/jetbrains/ide/HttpRequestHandler.java | 3 +++ .../org/jetbrains/io/DelegatingHttpRequestHandler.kt | 2 +- platform/platform-impl/src/org/jetbrains/io/netty.kt | 11 +++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java b/platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java index 5166e95ba8fd..815264ada9a8 100644 --- a/platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java +++ b/platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java @@ -44,6 +44,9 @@ public abstract class HttpRequestHandler { } @SuppressWarnings("SpellCheckingInspection") + /** + * Write request from browser without Origin will be always blocked regardles of your implementation. + */ public boolean isAccessible(@NotNull HttpRequest request) { String host = NettyKt.getHost(request); // If attacker.com DNS rebound to 127.0.0.1 and user open site directly — no Origin or Referer headers. diff --git a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt index b500c0294ab5..806d3e6a351b 100644 --- a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt +++ b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt @@ -38,7 +38,7 @@ internal class DelegatingHttpRequestHandler : DelegatingHttpRequestHandlerBase() request: FullHttpRequest, urlDecoder: QueryStringDecoder): Boolean { fun HttpRequestHandler.checkAndProcess(): Boolean { - return isSupported(request) && isAccessible(request) && process(urlDecoder, request, context) + return isSupported(request) && !request.isWriteFromBrowserWithoutOrigin() && isAccessible(request) && process(urlDecoder, request, context) } val prevHandlerAttribute = context.attr(PREV_HANDLER) diff --git a/platform/platform-impl/src/org/jetbrains/io/netty.kt b/platform/platform-impl/src/org/jetbrains/io/netty.kt index 9b88f0d50d7f..c01fc8febdd8 100644 --- a/platform/platform-impl/src/org/jetbrains/io/netty.kt +++ b/platform/platform-impl/src/org/jetbrains/io/netty.kt @@ -29,6 +29,7 @@ import io.netty.channel.socket.nio.NioServerSocketChannel import io.netty.channel.socket.oio.OioServerSocketChannel import io.netty.channel.socket.oio.OioSocketChannel import io.netty.handler.codec.http.HttpHeaderNames +import io.netty.handler.codec.http.HttpMethod import io.netty.handler.codec.http.HttpRequest import io.netty.handler.ssl.SslHandler import io.netty.resolver.HostsFileEntriesResolver @@ -112,6 +113,9 @@ val HttpRequest.origin: String? val HttpRequest.referrer: String? get() = headers().getAsString(HttpHeaderNames.REFERER) +val HttpRequest.userAgent: String? + get() = headers().getAsString(HttpHeaderNames.USER_AGENT) + inline fun ByteBuf.releaseIfError(task: () -> T): T { try { return task() @@ -178,4 +182,11 @@ fun parseAndCheckIsLocalHost(uri: String?, onlyAnyOrLoopback: Boolean = true, ho catch (ignored: Exception) { } return false +} + +// forbid POST requests from browser without Origin +fun HttpRequest.isWriteFromBrowserWithoutOrigin(): Boolean { + val userAgent = userAgent ?: return false + val method = method() + return origin.isNullOrEmpty() && userAgent.startsWith("Mozilla/5.0") && (method == HttpMethod.POST || method == HttpMethod.PATCH || method == HttpMethod.PUT || method == HttpMethod.DELETE) } \ No newline at end of file From 235eab07b7ce2358317d69739bc25ea84614fa41 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 15 Apr 2016 10:28:38 +0200 Subject: [PATCH 20/67] remove OpenFileXmlRpcHandler --- .../jetbrains/ide/OpenFileXmlRpcHandler.java | 48 ------------------- .../org/jetbrains/ide/XmlRpcServerImpl.java | 16 +------ .../com/intellij/ide/XmlRpcHandlerBean.java | 30 ------------ .../src/META-INF/PlatformExtensionPoints.xml | 7 +-- .../src/META-INF/built-in-server.xml | 2 - 5 files changed, 3 insertions(+), 100 deletions(-) delete mode 100644 platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java delete mode 100644 platform/platform-api/src/com/intellij/ide/XmlRpcHandlerBean.java diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java b/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java deleted file mode 100644 index 410bf62eb9d8..000000000000 --- a/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2000-2015 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jetbrains.ide; - -import com.intellij.openapi.diagnostic.Logger; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.concurrency.Promise; - -class OpenFileXmlRpcHandler { - private static final Logger LOG = Logger.getInstance(OpenFileXmlRpcHandler.class); - - // XML-RPC interface method - keep the signature intact - @SuppressWarnings("UnusedDeclaration") - public boolean open(String path) { - LOG.debug("open(" + path + ")"); - return doOpen(path, -1, -1); - } - - // XML-RPC interface method - keep the signature intact - @SuppressWarnings("UnusedDeclaration") - public boolean openAndNavigate(String path, int line, int column) { - LOG.debug("openAndNavigate(" + path + ", " + line + ", " + column + ")"); - return doOpen(path, line, column); - } - - private static boolean doOpen(@NotNull String path, int line, int column) { - OpenFileRequest request = new OpenFileRequest(); - request.setFile(path); - request.setLine(line); - request.setColumn(column); - request.setFocused(false); - Promise promise = HttpRequestHandler.EP_NAME.findExtension(OpenFileHttpService.class).openFile(request, null, null); - return promise != null && promise.getState() != Promise.State.REJECTED; - } -} \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/ide/XmlRpcServerImpl.java b/platform/built-in-server/src/org/jetbrains/ide/XmlRpcServerImpl.java index 3f26acc49bfd..782136dd77f9 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/XmlRpcServerImpl.java +++ b/platform/built-in-server/src/org/jetbrains/ide/XmlRpcServerImpl.java @@ -15,12 +15,8 @@ */ package org.jetbrains.ide; -import com.intellij.ide.XmlRpcHandlerBean; import com.intellij.ide.XmlRpcServer; -import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.extensions.AbstractExtensionPointBean; -import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.CharsetToolkit; import gnu.trove.THashMap; @@ -46,19 +42,9 @@ import java.util.Vector; public class XmlRpcServerImpl implements XmlRpcServer { private static final Logger LOG = Logger.getInstance(XmlRpcServerImpl.class); - private final Map handlerMapping; + private final Map handlerMapping = new THashMap(); public XmlRpcServerImpl() { - handlerMapping = new THashMap(); - for (XmlRpcHandlerBean handlerBean : Extensions.getExtensions(XmlRpcHandlerBean.EP_NAME)) { - try { - handlerMapping.put(handlerBean.name, AbstractExtensionPointBean.instantiate(handlerBean.findClass(handlerBean.implementation), ApplicationManager.getApplication().getPicoContainer(), true)); - } - catch (ClassNotFoundException e) { - LOG.error(e); - } - } - LOG.debug("XmlRpcServerImpl instantiated, handlers " + handlerMapping); } static final class XmlRpcRequestHandler extends HttpRequestHandler { diff --git a/platform/platform-api/src/com/intellij/ide/XmlRpcHandlerBean.java b/platform/platform-api/src/com/intellij/ide/XmlRpcHandlerBean.java deleted file mode 100644 index 77630f66b30e..000000000000 --- a/platform/platform-api/src/com/intellij/ide/XmlRpcHandlerBean.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2000-2015 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.intellij.ide; - -import com.intellij.openapi.extensions.AbstractExtensionPointBean; -import com.intellij.openapi.extensions.ExtensionPointName; -import com.intellij.util.xmlb.annotations.Attribute; - -public class XmlRpcHandlerBean extends AbstractExtensionPointBean { - public static final ExtensionPointName EP_NAME = ExtensionPointName.create("com.intellij.xmlRpcHandler"); - - @Attribute("name") - public String name; - - @Attribute("implementation") - public String implementation; -} \ No newline at end of file diff --git a/platform/platform-resources/src/META-INF/PlatformExtensionPoints.xml b/platform/platform-resources/src/META-INF/PlatformExtensionPoints.xml index 58990d478097..05e5bcead225 100644 --- a/platform/platform-resources/src/META-INF/PlatformExtensionPoints.xml +++ b/platform/platform-resources/src/META-INF/PlatformExtensionPoints.xml @@ -190,9 +190,6 @@ - - - @@ -215,11 +212,11 @@ - + - + diff --git a/platform/platform-resources/src/META-INF/built-in-server.xml b/platform/platform-resources/src/META-INF/built-in-server.xml index b38e318efdef..cc795eaf1080 100644 --- a/platform/platform-resources/src/META-INF/built-in-server.xml +++ b/platform/platform-resources/src/META-INF/built-in-server.xml @@ -22,8 +22,6 @@ - - From 0247ed2fe6b560e39288751c45861c9eed98a8c6 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 15 Apr 2016 13:03:20 +0200 Subject: [PATCH 21/67] cleanup --- .../src/org/jetbrains/io/NettyUtil.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/platform/platform-impl/src/org/jetbrains/io/NettyUtil.java b/platform/platform-impl/src/org/jetbrains/io/NettyUtil.java index 8bb15db8e996..33bd2693a253 100644 --- a/platform/platform-impl/src/org/jetbrains/io/NettyUtil.java +++ b/platform/platform-impl/src/org/jetbrains/io/NettyUtil.java @@ -19,10 +19,8 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Condition; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.BootstrapUtil; -import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.channel.socket.oio.OioSocketChannel; import io.netty.handler.codec.http.HttpMethod; @@ -200,22 +198,6 @@ public final class NettyUtil { (message.startsWith("Connection reset") || message.equals("Operation timed out") || message.equals("Connection timed out")); } - @SuppressWarnings("unused") - @Deprecated - @NotNull - public static ServerBootstrap nioServerBootstrap(@NotNull EventLoopGroup eventLoopGroup) { - ServerBootstrap bootstrap = new ServerBootstrap().group(eventLoopGroup).channel(NioServerSocketChannel.class); - bootstrap.childOption(ChannelOption.TCP_NODELAY, true).childOption(ChannelOption.SO_KEEPALIVE, true); - return bootstrap; - } - - @SuppressWarnings("unused") - @Deprecated - @NotNull - public static Bootstrap oioClientBootstrap() { - return NettyKt.oioClientBootstrap(); - } - public static Bootstrap nioClientBootstrap() { return nioClientBootstrap(new NioEventLoopGroup(1, PooledThreadExecutor.INSTANCE)); } From 865c83d5c99719e03865aab6c1bef19b9f8ba03c Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 15 Apr 2016 13:13:14 +0200 Subject: [PATCH 22/67] =?UTF-8?q?/api/file=20=E2=80=94=20do=20not=20expose?= =?UTF-8?q?=20status=20in=20any=20case,=20always=20return=20200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/org/jetbrains/builtInWebServer/StaticFileHandler.kt | 6 ++---- .../src/org/jetbrains/ide/OpenFileHttpService.kt | 5 ++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt index ef779182b485..2051af71edd4 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt @@ -13,7 +13,6 @@ import org.jetbrains.builtInWebServer.ssi.SsiExternalResolver import org.jetbrains.builtInWebServer.ssi.SsiProcessor import org.jetbrains.io.FileResponses import org.jetbrains.io.addKeepAliveIfNeed -import org.jetbrains.io.isLocalOrigin import org.jetbrains.io.send import java.nio.file.Files import java.nio.file.Path @@ -108,12 +107,11 @@ private fun sendIoFile(channel: Channel, file: Path, root: Path, request: HttpRe } } -fun checkAccess(channel: Channel, file: Path, request: HttpRequest, root: Path = file.root, doNotExposeStatusIfNotLocalOrigin: Boolean = false): Boolean { +fun checkAccess(channel: Channel, file: Path, request: HttpRequest, root: Path = file.root, doNotExposeStatus: Boolean = false): Boolean { var parent = file do { if (!hasAccess(parent)) { - (if (doNotExposeStatusIfNotLocalOrigin && !request.isLocalOrigin()) HttpResponseStatus.OK else HttpResponseStatus.FORBIDDEN) - .send(channel, request) + (if (doNotExposeStatus) HttpResponseStatus.OK else HttpResponseStatus.FORBIDDEN).send(channel, request) return false } parent = parent.parent ?: break diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt index caff2214a43c..d4242a3df3e9 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt +++ b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt @@ -42,7 +42,6 @@ import org.jetbrains.concurrency.AsyncPromise import org.jetbrains.concurrency.Promise import org.jetbrains.concurrency.catchError import org.jetbrains.concurrency.rejectedPromise -import org.jetbrains.io.isLocalOrigin import java.nio.file.Path import java.nio.file.Paths import java.util.concurrent.ConcurrentLinkedQueue @@ -123,7 +122,7 @@ internal class OpenFileHttpService : RestService() { .rejected { if (it === NOT_FOUND) { // don't expose file status if not local origin - sendStatus(if (request.isLocalOrigin()) HttpResponseStatus.NOT_FOUND else HttpResponseStatus.OK, keepAlive, channel) + sendStatus(HttpResponseStatus.OK, keepAlive, channel) } else { // todo send error @@ -141,7 +140,7 @@ internal class OpenFileHttpService : RestService() { if (!file.exists()) { return rejectedPromise(NOT_FOUND) } - return if (context == null || checkAccess(context.channel(), file, httpRequest!!, doNotExposeStatusIfNotLocalOrigin = true)) openAbsolutePath(file, request) else null + return if (context == null || checkAccess(context.channel(), file, httpRequest!!, doNotExposeStatus = true)) openAbsolutePath(file, request) else null } // we don't want to call refresh for each attempt on findFileByRelativePath call, so, we do what ourSaveAndSyncHandlerImpl does on frame activation From f765c8387593c6d0025632b375ee0fe594565303 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 15 Apr 2016 14:44:08 +0200 Subject: [PATCH 23/67] =?UTF-8?q?set=20X-Frame-Options=20to=20SameOrigin?= =?UTF-8?q?=20(rest=20api=20=E2=80=94=20Deny)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../built-in-server/src/org/jetbrains/ide/RestService.java | 4 +++- platform/platform-impl/src/org/jetbrains/io/Responses.kt | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/platform/built-in-server/src/org/jetbrains/ide/RestService.java b/platform/built-in-server/src/org/jetbrains/ide/RestService.java index 2d8beb522d5a..3ea42c11956a 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/RestService.java +++ b/platform/built-in-server/src/org/jetbrains/ide/RestService.java @@ -187,12 +187,14 @@ public abstract class RestService extends HttpRequestHandler { if (keepAlive) { HttpUtil.setKeepAlive(response, true); } + response.headers().set("X-Frame-Options", "Deny"); Responses.send(response, channel, !keepAlive); } - protected static void send(@NotNull BufferExposingByteArrayOutputStream byteOut, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) { + protected static void send(@NotNull BufferExposingByteArrayOutputStream byteOut, @NotNull HttpRequest request, @NotNull ChannelHandlerContext context) { HttpResponse response = Responses.response("application/json", Unpooled.wrappedBuffer(byteOut.getInternalBuffer(), 0, byteOut.size())); Responses.addNoCache(response); + response.headers().set("X-Frame-Options", "Deny"); Responses.send(response, context.channel(), request); } diff --git a/platform/platform-impl/src/org/jetbrains/io/Responses.kt b/platform/platform-impl/src/org/jetbrains/io/Responses.kt index 3c293b37f0d4..3393b936e7d0 100644 --- a/platform/platform-impl/src/org/jetbrains/io/Responses.kt +++ b/platform/platform-impl/src/org/jetbrains/io/Responses.kt @@ -99,6 +99,9 @@ fun HttpResponse.addKeepAliveIfNeed(request: HttpRequest): Boolean { fun HttpResponse.addCommonHeaders() { addServer() setDate() + if (!headers().contains("X-Frame-Options")) { + headers().set("X-Frame-Options", "SameOrigin") + } } fun HttpResponse.send(channel: Channel, close: Boolean) { From 99d82772c62627ff426b6817dbfc4c2a382288d1 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 15 Apr 2016 16:45:10 +0200 Subject: [PATCH 24/67] add X-Content-Type-Options: nosniff --- platform/platform-impl/src/org/jetbrains/io/Responses.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/platform/platform-impl/src/org/jetbrains/io/Responses.kt b/platform/platform-impl/src/org/jetbrains/io/Responses.kt index 3393b936e7d0..3efbd2597082 100644 --- a/platform/platform-impl/src/org/jetbrains/io/Responses.kt +++ b/platform/platform-impl/src/org/jetbrains/io/Responses.kt @@ -102,6 +102,7 @@ fun HttpResponse.addCommonHeaders() { if (!headers().contains("X-Frame-Options")) { headers().set("X-Frame-Options", "SameOrigin") } + headers().set("X-Content-Type-Options", "nosniff") } fun HttpResponse.send(channel: Channel, close: Boolean) { From 6efc6b5e307f40db17eefc9c7f756b571373f38a Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 15 Apr 2016 16:48:23 +0200 Subject: [PATCH 25/67] fix pubserve --- .../platform-impl/src/org/jetbrains/io/netty.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/platform/platform-impl/src/org/jetbrains/io/netty.kt b/platform/platform-impl/src/org/jetbrains/io/netty.kt index c01fc8febdd8..760f54b84bec 100644 --- a/platform/platform-impl/src/org/jetbrains/io/netty.kt +++ b/platform/platform-impl/src/org/jetbrains/io/netty.kt @@ -131,15 +131,13 @@ inline fun ByteBuf.releaseIfError(task: () -> T): T { } fun isLocalHost(host: String, onlyAnyOrLoopback: Boolean, hostsOnly: Boolean = false): Boolean { - if (onlyAnyOrLoopback) { - if (NetUtils.isLocalhost(host)) { - return true - } + if (NetUtils.isLocalhost(host)) { + return true + } - if (!InetAddresses.isInetAddress(host)) { - return false - } - // if IP address, it is safe to use getByName (not affected by DNS rebinding) + // if IP address, it is safe to use getByName (not affected by DNS rebinding) + if (onlyAnyOrLoopback && !InetAddresses.isInetAddress(host)) { + return false } fun InetAddress.isLocal() = isAnyLocalAddress || isLoopbackAddress || NetworkInterface.getByInetAddress(this) != null @@ -149,6 +147,7 @@ fun isLocalHost(host: String, onlyAnyOrLoopback: Boolean, hostsOnly: Boolean = f if (!address.isLocal()) { return false } + // be aware - on windows hosts file doesn't contain localhost // hosts can contain remote addresses, so, we check it if (hostsOnly && !InetAddresses.isInetAddress(host)) { return HostsFileEntriesResolver.DEFAULT.address(host).let { it != null && it.isLocal() } From 40a7b465c5be73a6202ab5ee2b3dcf45a5a986e8 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 15 Apr 2016 18:02:48 +0200 Subject: [PATCH 26/67] log.warn if not found, fix comment --- .../src/org/jetbrains/ide/OpenFileHttpService.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt index d4242a3df3e9..8417d3dbcddb 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt +++ b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt @@ -121,8 +121,9 @@ internal class OpenFileHttpService : RestService() { promise.done { sendStatus(HttpResponseStatus.OK, keepAlive, channel) } .rejected { if (it === NOT_FOUND) { - // don't expose file status if not local origin + // don't expose file status sendStatus(HttpResponseStatus.OK, keepAlive, channel) + LOG.warn("File ${apiRequest.file} not found") } else { // todo send error From 8c949bdba2def6f2cb9932641173256fbec40911 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 18 Apr 2016 12:39:16 +0200 Subject: [PATCH 27/67] =?UTF-8?q?checkAccess=20in=20DefaultWebServerPathHa?= =?UTF-8?q?ndler=20=E2=80=94=20because=20each=20implementation=20of=20WebS?= =?UTF-8?q?erverFileHandler=20must=20do=20it=20in=20any=20case?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../builtInWebServer/BuiltInWebServer.kt | 3 +- .../DefaultWebServerPathHandler.kt | 28 +++++++++++++++++++ .../builtInWebServer/StaticFileHandler.kt | 27 ++++-------------- .../org/jetbrains/ide/OpenFileHttpService.kt | 5 ++-- .../src/org/jetbrains/io/Responses.kt | 3 ++ 5 files changed, 42 insertions(+), 24 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index 2e2c7395c70a..fae51c09cf8c 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -138,7 +138,8 @@ private fun doProcess(request: FullHttpRequest, context: ChannelHandlerContext, val path = toIdeaPath(decodedPath, offset) if (path == null) { - HttpResponseStatus.BAD_REQUEST.send(context.channel(), request) + LOG.warn("$decodedPath is not valid") + HttpResponseStatus.NOT_FOUND.send(context.channel(), request) return true } diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt index 128e3be572dc..a0bf723a0018 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt @@ -20,13 +20,18 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.io.endsWithName import com.intellij.openapi.util.io.endsWithSlash import com.intellij.openapi.util.io.getParentPath +import com.intellij.openapi.vfs.VFileProperty import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.PathUtilRt +import com.intellij.util.isDirectory +import io.netty.channel.Channel import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.FullHttpRequest +import io.netty.handler.codec.http.HttpRequest import io.netty.handler.codec.http.HttpResponseStatus import org.jetbrains.io.send import java.nio.file.Path +import java.nio.file.Paths private class DefaultWebServerPathHandler : WebServerPathHandler() { override fun process(path: String, @@ -94,6 +99,10 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { } } + if (!checkAccess(pathInfo, channel, request)) { + return true + } + val canonicalPath = if (indexUsed) "$path/${pathInfo.name}" else path for (fileHandler in WebServerFileHandler.EP_NAME.extensions) { LOG.catchAndLog { @@ -104,4 +113,23 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { } return false } +} + +private fun checkAccess(pathInfo: PathInfo, channel: Channel, request: HttpRequest): Boolean { + if (pathInfo.ioFile != null || pathInfo.file!!.isInLocalFileSystem) { + val file = pathInfo.ioFile ?: Paths.get(pathInfo.file!!.path) + if (file.isDirectory()) { + HttpResponseStatus.NOT_FOUND.send(channel, request) + return false + } + else if (!checkAccess(channel, file, request, Paths.get(pathInfo.root.path))) { + return false + } + } + else if (pathInfo.file!!.`is`(VFileProperty.HIDDEN)) { + HttpResponseStatus.NOT_FOUND.send(channel, request) + return false + } + + return true } \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt index 2051af71edd4..d1b67c2f6cd9 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt @@ -1,9 +1,7 @@ package org.jetbrains.builtInWebServer import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VFileProperty import com.intellij.util.PathUtilRt -import com.intellij.util.isDirectory import io.netty.buffer.ByteBufUtf8Writer import io.netty.channel.Channel import io.netty.channel.ChannelFutureListener @@ -13,6 +11,7 @@ import org.jetbrains.builtInWebServer.ssi.SsiExternalResolver import org.jetbrains.builtInWebServer.ssi.SsiProcessor import org.jetbrains.io.FileResponses import org.jetbrains.io.addKeepAliveIfNeed +import org.jetbrains.io.okInSafeMode import org.jetbrains.io.send import java.nio.file.Files import java.nio.file.Path @@ -31,15 +30,10 @@ private class StaticFileHandler : WebServerFileHandler() { return true } - sendIoFile(channel, ioFile, Paths.get(pathInfo.root.path), request) + FileResponses.sendFile(request, channel, ioFile) } else { val file = pathInfo.file!! - if (file.`is`(VFileProperty.HIDDEN)) { - HttpResponseStatus.FORBIDDEN.send(channel, request) - return true - } - val response = FileResponses.prepareSend(request, channel, file.timeStamp, file.name) ?: return true val keepAlive = response.addKeepAliveIfNeed(request) @@ -98,20 +92,11 @@ private class StaticFileHandler : WebServerFileHandler() { } } -private fun sendIoFile(channel: Channel, file: Path, root: Path, request: HttpRequest) { - if (file.isDirectory()) { - HttpResponseStatus.FORBIDDEN.send(channel, request) - } - else if (checkAccess(channel, file, request, root)) { - FileResponses.sendFile(request, channel, file) - } -} - -fun checkAccess(channel: Channel, file: Path, request: HttpRequest, root: Path = file.root, doNotExposeStatus: Boolean = false): Boolean { +internal fun checkAccess(channel: Channel, file: Path, request: HttpRequest, root: Path = file.root): Boolean { var parent = file do { if (!hasAccess(parent)) { - (if (doNotExposeStatus) HttpResponseStatus.OK else HttpResponseStatus.FORBIDDEN).send(channel, request) + HttpResponseStatus.FORBIDDEN.okInSafeMode().send(channel, request) return false } parent = parent.parent ?: break @@ -120,5 +105,5 @@ fun checkAccess(channel: Channel, file: Path, request: HttpRequest, root: Path = return true } -// deny access to .htaccess files -private fun hasAccess(result: Path) = Files.isReadable(result) && !(Files.isHidden(result) || result.fileName.toString().startsWith(".ht")) \ No newline at end of file +// deny access to any dot prefixed file +private fun hasAccess(result: Path) = Files.isReadable(result) && !(Files.isHidden(result) || result.fileName.toString().startsWith('.')) \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt index 8417d3dbcddb..96ca5601edbb 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt +++ b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt @@ -42,6 +42,7 @@ import org.jetbrains.concurrency.AsyncPromise import org.jetbrains.concurrency.Promise import org.jetbrains.concurrency.catchError import org.jetbrains.concurrency.rejectedPromise +import org.jetbrains.io.okInSafeMode import java.nio.file.Path import java.nio.file.Paths import java.util.concurrent.ConcurrentLinkedQueue @@ -122,7 +123,7 @@ internal class OpenFileHttpService : RestService() { .rejected { if (it === NOT_FOUND) { // don't expose file status - sendStatus(HttpResponseStatus.OK, keepAlive, channel) + sendStatus(HttpResponseStatus.NOT_FOUND.okInSafeMode(), keepAlive, channel) LOG.warn("File ${apiRequest.file} not found") } else { @@ -141,7 +142,7 @@ internal class OpenFileHttpService : RestService() { if (!file.exists()) { return rejectedPromise(NOT_FOUND) } - return if (context == null || checkAccess(context.channel(), file, httpRequest!!, doNotExposeStatus = true)) openAbsolutePath(file, request) else null + return if (context == null || checkAccess(context.channel(), file, httpRequest!!)) openAbsolutePath(file, request) else null } // we don't want to call refresh for each attempt on findFileByRelativePath call, so, we do what ourSaveAndSyncHandlerImpl does on frame activation diff --git a/platform/platform-impl/src/org/jetbrains/io/Responses.kt b/platform/platform-impl/src/org/jetbrains/io/Responses.kt index 3efbd2597082..4a3bd36e3523 100644 --- a/platform/platform-impl/src/org/jetbrains/io/Responses.kt +++ b/platform/platform-impl/src/org/jetbrains/io/Responses.kt @@ -103,6 +103,7 @@ fun HttpResponse.addCommonHeaders() { headers().set("X-Frame-Options", "SameOrigin") } headers().set("X-Content-Type-Options", "nosniff") + headers().set("x-xss-protection", "1; mode=block") } fun HttpResponse.send(channel: Channel, close: Boolean) { @@ -125,6 +126,8 @@ fun HttpResponseStatus.send(channel: Channel, request: HttpRequest? = null, desc createStatusResponse(this, request, description).send(channel, request) } +fun HttpResponseStatus.okInSafeMode() = if (ApplicationManager.getApplication()?.isUnitTestMode ?: false) this else HttpResponseStatus.OK + private fun createStatusResponse(responseStatus: HttpResponseStatus, request: HttpRequest?, description: String?): HttpResponse { if (request != null && request.method() === HttpMethod.HEAD) { return responseStatus.response() From 68063e28d1f0e5ac8c590e71cf07b870e1b002fd Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 18 Apr 2016 13:31:47 +0200 Subject: [PATCH 28/67] any asset must be requested with valid Refer (from regular browser) --- .../builtInWebServer/BuiltInWebServer.kt | 21 ++++++++++++++++--- .../builtInWebServer/StaticFileHandler.kt | 2 ++ .../builtInWebServer/WebServerFileHandler.kt | 4 +++- .../src/org/jetbrains/io/netty.kt | 4 +++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index fae51c09cf8c..a754c1dcb5c6 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -22,6 +22,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.util.SystemInfoRt import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.io.FileUtilRt import com.intellij.openapi.util.io.endsWithName import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.UriUtil @@ -32,9 +33,7 @@ import com.intellij.util.net.NetUtils import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.* import org.jetbrains.ide.HttpRequestHandler -import org.jetbrains.io.host -import org.jetbrains.io.isLocalOrigin -import org.jetbrains.io.send +import org.jetbrains.io.* import java.io.IOException import java.net.InetAddress import java.nio.file.Path @@ -143,6 +142,11 @@ private fun doProcess(request: FullHttpRequest, context: ChannelHandlerContext, return true } + if (request.origin == null && request.referrer == null && request.isRegularBrowser() && !canBeAccessedDirectly(path)) { + HttpResponseStatus.NOT_FOUND.send(context.channel(), request) + return true + } + for (pathHandler in WebServerPathHandler.EP_NAME.extensions) { LOG.catchAndLog { if (pathHandler.process(path, project, request, context, projectName, decodedPath, isCustomHost)) { @@ -243,4 +247,15 @@ internal fun isOwnHostName(host: String): Boolean { catch (ignored: IOException) { return false } +} + +private fun canBeAccessedDirectly(path: String): Boolean { + for (fileHandler in WebServerFileHandler.EP_NAME.extensions) { + for (ext in fileHandler.pageFileExtensions) { + if (FileUtilRt.extensionEquals(path, ext)) { + return true + } + } + } + return false } \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt index d1b67c2f6cd9..2158b67948d6 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt @@ -18,6 +18,8 @@ import java.nio.file.Path import java.nio.file.Paths private class StaticFileHandler : WebServerFileHandler() { + override val pageFileExtensions = arrayOf("html", "htm", "shtml") + private var ssiProcessor: SsiProcessor? = null override fun process(pathInfo: PathInfo, canonicalPath: CharSequence, project: Project, request: FullHttpRequest, channel: Channel, projectNameIfNotCustomHost: String?): Boolean { diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerFileHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerFileHandler.kt index d2eed3717aca..1e823c4861a4 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerFileHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerFileHandler.kt @@ -25,6 +25,9 @@ abstract class WebServerFileHandler { internal val EP_NAME = ExtensionPointName.create("org.jetbrains.webServerFileHandler") } + open val pageFileExtensions: Array + get() = emptyArray() + /** * canonicalRequestPath contains index file name (if not specified in the request) */ @@ -34,7 +37,6 @@ abstract class WebServerFileHandler { request: FullHttpRequest, channel: Channel, projectNameIfNotCustomHost: String?): Boolean - } fun getRequestPath(canonicalPath: CharSequence, projectNameIfNotCustomHost: String?) = if (projectNameIfNotCustomHost == null) "/$canonicalPath" else "/$projectNameIfNotCustomHost/$canonicalPath" \ No newline at end of file diff --git a/platform/platform-impl/src/org/jetbrains/io/netty.kt b/platform/platform-impl/src/org/jetbrains/io/netty.kt index 760f54b84bec..fd09296971cd 100644 --- a/platform/platform-impl/src/org/jetbrains/io/netty.kt +++ b/platform/platform-impl/src/org/jetbrains/io/netty.kt @@ -183,9 +183,11 @@ fun parseAndCheckIsLocalHost(uri: String?, onlyAnyOrLoopback: Boolean = true, ho return false } +fun HttpRequest.isRegularBrowser() = userAgent?.startsWith("Mozilla/5.0") ?: false + // forbid POST requests from browser without Origin fun HttpRequest.isWriteFromBrowserWithoutOrigin(): Boolean { val userAgent = userAgent ?: return false val method = method() - return origin.isNullOrEmpty() && userAgent.startsWith("Mozilla/5.0") && (method == HttpMethod.POST || method == HttpMethod.PATCH || method == HttpMethod.PUT || method == HttpMethod.DELETE) + return origin.isNullOrEmpty() && isRegularBrowser() && (method == HttpMethod.POST || method == HttpMethod.PATCH || method == HttpMethod.PUT || method == HttpMethod.DELETE) } \ No newline at end of file From 63ad820e63f9b165c70ec4a545e9332b0e6369b8 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 18 Apr 2016 16:01:17 +0200 Subject: [PATCH 29/67] use UUID instead of incremental int id --- .../git4idea/http/GitAskPassApp.java | 14 ++++---- .../git4idea/http/GitAskPassXmlRpcClient.java | 10 +++--- .../http/GitAskPassXmlRpcHandler.java | 10 +++--- .../jetbrains/git4idea/ssh/GitSSHHandler.java | 34 ++++++++----------- .../git4idea/ssh/GitSSHXmlRpcClient.java | 30 ++++++++-------- .../org/jetbrains/git4idea/ssh/SSHMain.java | 6 ++-- .../src/git4idea/commands/GitHandler.java | 14 ++++---- .../git4idea/commands/GitHttpAuthService.java | 11 +++--- .../rebase/GitRebaseEditorService.java | 5 +-- .../git4idea/ssh/GitXmlRpcHandlerService.java | 15 ++++---- .../git4idea/ssh/GitXmlRpcSshService.java | 27 ++++++++------- 11 files changed, 86 insertions(+), 90 deletions(-) diff --git a/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassApp.java b/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassApp.java index 8c8e425570a2..8ce927804e52 100644 --- a/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassApp.java +++ b/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassApp.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2013 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,16 +56,16 @@ public class GitAskPassApp implements GitExternalApp { boolean usernameNeeded = arguments.getFirst(); String url = arguments.getSecond(); - int handler = Integer.parseInt(getNotNull(GitAskPassXmlRpcHandler.GIT_ASK_PASS_HANDLER_ENV)); + String token = getNotNull(GitAskPassXmlRpcHandler.GIT_ASK_PASS_HANDLER_ENV); int xmlRpcPort = Integer.parseInt(getNotNull(GitAskPassXmlRpcHandler.GIT_ASK_PASS_PORT_ENV)); GitAskPassXmlRpcClient xmlRpcClient = new GitAskPassXmlRpcClient(xmlRpcPort); if (usernameNeeded) { - String username = xmlRpcClient.askUsername(handler, url); + String username = xmlRpcClient.askUsername(token, url); System.out.println(username); } else { - String pass = xmlRpcClient.askPassword(handler, url); + String pass = xmlRpcClient.askPassword(token, url); System.out.println(pass); } } @@ -77,11 +77,11 @@ public class GitAskPassApp implements GitExternalApp { @NotNull private static String getNotNull(@NotNull String env) { - String handlerValue = System.getenv(env); - if (handlerValue == null) { + String value = System.getenv(env); + if (value == null) { throw new IllegalStateException(env + " environment variable is not defined!"); } - return handlerValue; + return value; } @NotNull diff --git a/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassXmlRpcClient.java b/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassXmlRpcClient.java index 5d3f8681f099..85e49697416e 100644 --- a/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassXmlRpcClient.java +++ b/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassXmlRpcClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2013 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,9 +38,9 @@ class GitAskPassXmlRpcClient { // Obsolete collection usage because of the XmlRpcClientLite API @SuppressWarnings({"UseOfObsoleteCollectionType", "unchecked"}) - String askUsername(int handler, @NotNull String url) { + String askUsername(String token, @NotNull String url) { Vector parameters = new Vector(); - parameters.add(handler); + parameters.add(token); parameters.add(url); try { @@ -56,9 +56,9 @@ class GitAskPassXmlRpcClient { // Obsolete collection usage because of the XmlRpcClientLite API @SuppressWarnings({"UseOfObsoleteCollectionType", "unchecked"}) - String askPassword(int handler, @NotNull String url) { + String askPassword(String token, @NotNull String url) { Vector parameters = new Vector(); - parameters.add(handler); + parameters.add(token); parameters.add(url); try { diff --git a/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassXmlRpcHandler.java b/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassXmlRpcHandler.java index 36cc365c1d80..9c9900a1f3ca 100644 --- a/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassXmlRpcHandler.java +++ b/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassXmlRpcHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2013 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,25 +31,25 @@ public interface GitAskPassXmlRpcHandler { /** * Get the username from the user to access the given URL. - * @param handler XML RPC handler number. + * @param token Access token. * @param url URL which Git tries to access. * @return The Username which should be used for the URL. */ // UnusedDeclaration suppressed: the method is used via XML RPC @SuppressWarnings("UnusedDeclaration") @NotNull - String askUsername(int handler, @NotNull String url); + String askUsername(String token, @NotNull String url); /** * Get the password from the user to access the given URL. * It is assumed that the username either is specified in the URL (http://username@host.com), or has been asked earlier. - * @param handler XML RPC handler number. + * @param token Access token. * @param url URL which Git tries to access. * @return The password which should be used for the URL. */ // UnusedDeclaration suppressed: the method is used via XML RPC @SuppressWarnings("UnusedDeclaration") @NotNull - String askPassword(int handler, @NotNull String url); + String askPassword(String token, @NotNull String url); } diff --git a/plugins/git4idea/rt/src/org/jetbrains/git4idea/ssh/GitSSHHandler.java b/plugins/git4idea/rt/src/org/jetbrains/git4idea/ssh/GitSSHHandler.java index 2bc849d83fb0..4ce4ea31b6a2 100644 --- a/plugins/git4idea/rt/src/org/jetbrains/git4idea/ssh/GitSSHHandler.java +++ b/plugins/git4idea/rt/src/org/jetbrains/git4idea/ssh/GitSSHHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2012 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,16 +29,10 @@ public interface GitSSHHandler { */ @NonNls String GIT_SSH_PREFIX = "git-ssh-"; /** - * Name of environment variable for SSH handler number + * Name of environment variable for SSH handler access token */ @NonNls String SSH_HANDLER_ENV = "GIT4IDEA_SSH_HANDLER"; - /** - * Name of environment variable for SSH handler number - */ @NonNls String SSH_IGNORE_KNOWN_HOSTS_ENV = "GIT4IDEA_SSH_IGNORE_KNOWN_HOSTS"; - /** - * Name of environment variable for SSH handler - */ @NonNls String SSH_PORT_ENV = "GIT4IDEA_SSH_PORT"; /** * Name of environment variable for SSH executable @@ -60,7 +54,7 @@ public interface GitSSHHandler { /** * Verify server host key * - * @param handler a handler identifier + * @param token Access token. * @param hostName a host name * @param port a port number * @param serverHostKeyAlgorithm an algorithm @@ -68,7 +62,7 @@ public interface GitSSHHandler { * @param isNew true if the key is a new, false if the key was changed * @return true the host is verified, false otherwise */ - boolean verifyServerHostKey(int handler, + boolean verifyServerHostKey(String token, String hostName, int port, String serverHostKeyAlgorithm, @@ -78,7 +72,7 @@ public interface GitSSHHandler { /** * Ask passphrase for the key * - * @param handler a handler identifier + * @param token Access token. * @param userName a name of user * @param keyPath a path for the key * @param resetPassword a reset password if one was stored in password database @@ -86,12 +80,12 @@ public interface GitSSHHandler { * @return the passphrase entered by the user */ @Nullable - String askPassphrase(final int handler, final String userName, final String keyPath, boolean resetPassword, final String lastError); + String askPassphrase(String token, final String userName, final String keyPath, boolean resetPassword, final String lastError); /** * Reply to challenge for keyboard-interactive method. Also used for * - * @param handlerNo a handler identifier + * @param token Access token. * @param userName a user name (includes host and port) * @param name name of challenge * @param instruction instruction @@ -103,7 +97,7 @@ public interface GitSSHHandler { */ @SuppressWarnings({"UseOfObsoleteCollectionType"}) @Nullable - Vector replyToChallenge(final int handlerNo, + Vector replyToChallenge(String token, final String userName, final String name, final String instruction, @@ -115,19 +109,19 @@ public interface GitSSHHandler { /** * Ask password for the specified user name * - * @param handlerNo a handler identifier + * @param token Access token. * @param userName a name of user to ask password for * @param resetPassword a reset password if one was stored in password database * @param lastError a last error * @return the password or null if authentication failed. */ @Nullable - String askPassword(final int handlerNo, final String userName, boolean resetPassword, final String lastError); + String askPassword(String token, final String userName, boolean resetPassword, final String lastError); /** * Notify invoker about last successful authentication attempt. * - * @param handlerNo the handler + * @param token the handler * @param userName the user name * @param method the authentication method, the empty string means that authentication failed * @param error the error shown in the case when authentication process failed @@ -135,14 +129,14 @@ public interface GitSSHHandler { * @return The method doesn't return any sensible value, but it is needed here, since the Apache XML-RPC implementation which we use * doesn't allow void methods: "IllegalArgumentException: void return types for handler methods not supported". */ - String setLastSuccessful(final int handlerNo, final String userName, final String method, final String error); + String setLastSuccessful(String token, final String userName, final String method, final String error); /** * Get last successful authentication method * - * @param handlerNo the handler no + * @param token Access token * @param userName the user name * @return the authentication method, the empty string means that last authentication failed */ - String getLastSuccessful(final int handlerNo, final String userName); + String getLastSuccessful(String token, final String userName); } diff --git a/plugins/git4idea/rt/src/org/jetbrains/git4idea/ssh/GitSSHXmlRpcClient.java b/plugins/git4idea/rt/src/org/jetbrains/git4idea/ssh/GitSSHXmlRpcClient.java index c4ddd216e1cd..593362a3ee8e 100644 --- a/plugins/git4idea/rt/src/org/jetbrains/git4idea/ssh/GitSSHXmlRpcClient.java +++ b/plugins/git4idea/rt/src/org/jetbrains/git4idea/ssh/GitSSHXmlRpcClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2012 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,7 +49,7 @@ public class GitSSHXmlRpcClient implements GitSSHHandler { * {@inheritDoc} */ @SuppressWarnings("unchecked") - public boolean verifyServerHostKey(final int handler, + public boolean verifyServerHostKey(String token, final String hostname, final int port, final String serverHostKeyAlgorithm, @@ -59,7 +59,7 @@ public class GitSSHXmlRpcClient implements GitSSHHandler { return false; } Vector parameters = new Vector(); - parameters.add(handler); + parameters.add(token); parameters.add(hostname); parameters.add(port); parameters.add(serverHostKeyAlgorithm); @@ -91,7 +91,7 @@ public class GitSSHXmlRpcClient implements GitSSHHandler { */ @Nullable @SuppressWarnings("unchecked") - public String askPassphrase(final int handler, + public String askPassphrase(String token, final String username, final String keyPath, final boolean resetPassword, @@ -100,7 +100,7 @@ public class GitSSHXmlRpcClient implements GitSSHHandler { return null; } Vector parameters = new Vector(); - parameters.add(handler); + parameters.add(token); parameters.add(username); parameters.add(keyPath); parameters.add(resetPassword); @@ -121,7 +121,7 @@ public class GitSSHXmlRpcClient implements GitSSHHandler { */ @Nullable @SuppressWarnings("unchecked") - public Vector replyToChallenge(final int handlerNo, + public Vector replyToChallenge(String token, final String username, final String name, final String instruction, @@ -133,7 +133,7 @@ public class GitSSHXmlRpcClient implements GitSSHHandler { return null; } Vector parameters = new Vector(); - parameters.add(handlerNo); + parameters.add(token); parameters.add(username); parameters.add(name); parameters.add(instruction); @@ -157,12 +157,12 @@ public class GitSSHXmlRpcClient implements GitSSHHandler { */ @Nullable @SuppressWarnings("unchecked") - public String askPassword(final int handlerNo, final String username, final boolean resetPassword, final String lastError) { + public String askPassword(String token, final String username, final boolean resetPassword, final String lastError) { if (myClient == null) { return null; } Vector parameters = new Vector(); - parameters.add(handlerNo); + parameters.add(token); parameters.add(username); parameters.add(resetPassword); parameters.add(lastError); @@ -179,12 +179,12 @@ public class GitSSHXmlRpcClient implements GitSSHHandler { @Override @SuppressWarnings("unchecked") - public String setLastSuccessful(int handlerNo, String userName, String method, String error) { + public String setLastSuccessful(String token, String userName, String method, String error) { if (myClient == null) { return ""; } Vector parameters = new Vector(); - parameters.add(handlerNo); + parameters.add(token); parameters.add(userName); parameters.add(method); parameters.add(error); @@ -204,22 +204,22 @@ public class GitSSHXmlRpcClient implements GitSSHHandler { */ @Override @SuppressWarnings("unchecked") - public String getLastSuccessful(int handlerNo, String userName) { + public String getLastSuccessful(String token, String userName) { if (myClient == null) { return ""; } Vector parameters = new Vector(); - parameters.add(handlerNo); + parameters.add(token); parameters.add(userName); try { return (String)myClient.execute(methodName("getLastSuccessful"), parameters); } catch (XmlRpcException e) { - log("getLastSuccessful failed. handlerNo: " + handlerNo + ", userName: " + userName + ", client: " + myClient.getURL()); + log("getLastSuccessful failed. token: " + token + ", userName: " + userName + ", client: " + myClient.getURL()); throw new RuntimeException("Invocation failed " + e.getMessage(), e); } catch (IOException e) { - log("getLastSuccessful failed. handlerNo: " + handlerNo + ", userName: " + userName + ", client: " + myClient.getURL()); + log("getLastSuccessful failed. token: " + token + ", userName: " + userName + ", client: " + myClient.getURL()); throw new RuntimeException("Invocation failed " + e.getMessage(), e); } } diff --git a/plugins/git4idea/rt/src/org/jetbrains/git4idea/ssh/SSHMain.java b/plugins/git4idea/rt/src/org/jetbrains/git4idea/ssh/SSHMain.java index b5bc260b9520..67a74b92b0ac 100644 --- a/plugins/git4idea/rt/src/org/jetbrains/git4idea/ssh/SSHMain.java +++ b/plugins/git4idea/rt/src/org/jetbrains/git4idea/ssh/SSHMain.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2012 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ public class SSHMain implements GitExternalApp { /** * Handler number */ - private final int myHandlerNo; + private final String myHandlerNo; /** * the xml RPC port */ @@ -120,7 +120,7 @@ public class SSHMain implements GitExternalApp { private SSHMain(String host, String username, Integer port, String command) throws IOException { SSHConfig config = SSHConfig.load(); myHost = config.lookup(username, host, port); - myHandlerNo = Integer.parseInt(System.getenv(GitSSHHandler.SSH_HANDLER_ENV)); + myHandlerNo = System.getenv(GitSSHHandler.SSH_HANDLER_ENV); int xmlRpcPort = Integer.parseInt(System.getenv(GitSSHHandler.SSH_PORT_ENV)); myXmlRpcClient = new GitSSHXmlRpcClient(xmlRpcPort, myHost.isBatchMode()); myCommand = command; diff --git a/plugins/git4idea/src/git4idea/commands/GitHandler.java b/plugins/git4idea/src/git4idea/commands/GitHandler.java index ef234be88087..52aec9f6388e 100644 --- a/plugins/git4idea/src/git4idea/commands/GitHandler.java +++ b/plugins/git4idea/src/git4idea/commands/GitHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2014 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -89,8 +89,8 @@ public abstract class GitHandler { private final File myWorkingDirectory; private boolean myEnvironmentCleanedUp = true; // the flag indicating that environment has been cleaned up, by default is true because there is nothing to clean - private int mySshHandler = -1; - private int myHttpHandler = -1; + private UUID mySshHandler; + private UUID myHttpHandler; private Processor myInputProcessor; // The processor for stdin // if true process might be cancelled @@ -463,7 +463,7 @@ public abstract class GitHandler { GitHttpAuthenticator httpAuthenticator = service.createAuthenticator(myProject, myCommand, ObjectUtils.assertNotNull(myUrls)); myHttpHandler = service.registerHandler(httpAuthenticator, myProject); myEnvironmentCleanedUp = false; - myEnv.put(GitAskPassXmlRpcHandler.GIT_ASK_PASS_HANDLER_ENV, Integer.toString(myHttpHandler)); + myEnv.put(GitAskPassXmlRpcHandler.GIT_ASK_PASS_HANDLER_ENV, myHttpHandler.toString()); int port = service.getXmlRcpPort(); myEnv.put(GitAskPassXmlRpcHandler.GIT_ASK_PASS_PORT_ENV, Integer.toString(port)); LOG.debug(String.format("handler=%s, port=%s", myHttpHandler, port)); @@ -475,7 +475,7 @@ public abstract class GitHandler { myEnv.put(GitSSHHandler.GIT_SSH_ENV, ssh.getScriptPath().getPath()); mySshHandler = ssh.registerHandler(new GitSSHGUIHandler(myProject), myProject); myEnvironmentCleanedUp = false; - myEnv.put(GitSSHHandler.SSH_HANDLER_ENV, Integer.toString(mySshHandler)); + myEnv.put(GitSSHHandler.SSH_HANDLER_ENV, mySshHandler.toString()); int port = ssh.getXmlRcpPort(); myEnv.put(GitSSHHandler.SSH_PORT_ENV, Integer.toString(port)); LOG.debug(String.format("handler=%s, port=%s", mySshHandler, port)); @@ -602,10 +602,10 @@ public abstract class GitHandler { if (myEnvironmentCleanedUp) { return; } - if (mySshHandler >= 0) { + if (mySshHandler != null) { ServiceManager.getService(GitXmlRpcSshService.class).unregisterHandler(mySshHandler); } - if (myHttpHandler >= 0) { + if (myHttpHandler != null) { ServiceManager.getService(GitHttpAuthService.class).unregisterHandler(myHttpHandler); } myEnvironmentCleanedUp = true; diff --git a/plugins/git4idea/src/git4idea/commands/GitHttpAuthService.java b/plugins/git4idea/src/git4idea/commands/GitHttpAuthService.java index 6ddbcab22147..7cc1a8deb06c 100644 --- a/plugins/git4idea/src/git4idea/commands/GitHttpAuthService.java +++ b/plugins/git4idea/src/git4idea/commands/GitHttpAuthService.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2013 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.jetbrains.git4idea.ssh.GitXmlRpcHandlerService; import org.jetbrains.git4idea.util.ScriptGenerator; import java.util.Collection; +import java.util.UUID; /** * Provides the authentication mechanism for Git HTTP connections. @@ -57,14 +58,14 @@ public abstract class GitHttpAuthService extends GitXmlRpcHandlerServiceThe provider of external application scripts called by Git when a remote operation needs communication with the user.

@@ -57,8 +58,7 @@ public abstract class GitXmlRpcHandlerService { @Nullable private File myScriptPath; @NotNull private final Object SCRIPT_FILE_LOCK = new Object(); - @NotNull private final THashMap handlers = new THashMap(); - private int myNextHandlerKey; + @NotNull private final THashMap handlers = new THashMap(); @NotNull private final Object HANDLERS_LOCK = new Object(); /** @@ -111,14 +111,14 @@ public abstract class GitXmlRpcHandlerService { * @param parentDisposable a disposable to unregister the handler if it doesn't get unregistered manually * @return an identifier to pass to the environment variable */ - public int registerHandler(@NotNull T handler, @NotNull Disposable parentDisposable) { + public UUID registerHandler(@NotNull T handler, @NotNull Disposable parentDisposable) { synchronized (HANDLERS_LOCK) { XmlRpcServer xmlRpcServer = XmlRpcServer.SERVICE.getInstance(); if (!xmlRpcServer.hasHandler(myHandlerName)) { xmlRpcServer.addHandler(myHandlerName, createRpcRequestHandlerDelegate()); } - final int key = myNextHandlerKey; + final UUID key = UUID.randomUUID(); handlers.put(key, handler); Disposer.register(parentDisposable, new Disposable() { @Override @@ -126,7 +126,6 @@ public abstract class GitXmlRpcHandlerService { handlers.remove(key); } }); - myNextHandlerKey++; return key; } } @@ -146,7 +145,7 @@ public abstract class GitXmlRpcHandlerService { * @return the registered handler */ @NotNull - protected T getHandler(int key) { + protected T getHandler(UUID key) { synchronized (HANDLERS_LOCK) { T rc = handlers.get(key); if (rc == null) { @@ -161,7 +160,7 @@ public abstract class GitXmlRpcHandlerService { * * @param key the key to unregister */ - public void unregisterHandler(int key) { + public void unregisterHandler(UUID key) { synchronized (HANDLERS_LOCK) { if (handlers.remove(key) == null) { throw new IllegalArgumentException("The handler " + key + " is not registered"); diff --git a/plugins/git4idea/src/org/jetbrains/git4idea/ssh/GitXmlRpcSshService.java b/plugins/git4idea/src/org/jetbrains/git4idea/ssh/GitXmlRpcSshService.java index 5d802d36e065..9b03e6db8c94 100644 --- a/plugins/git4idea/src/org/jetbrains/git4idea/ssh/GitXmlRpcSshService.java +++ b/plugins/git4idea/src/org/jetbrains/git4idea/ssh/GitXmlRpcSshService.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2013 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import git4idea.commands.GitSSHGUIHandler; import org.jetbrains.annotations.NotNull; import org.jetbrains.git4idea.util.ScriptGenerator; +import java.util.UUID; import java.util.Vector; /** @@ -49,37 +50,37 @@ public class GitXmlRpcSshService extends GitXmlRpcHandlerService replyToChallenge(int handlerNo, String username, String name, String instruction, int numPrompts, + public Vector replyToChallenge(String token, String username, String name, String instruction, int numPrompts, Vector prompt, Vector echo, String lastError) { - return adjustNull(getHandler(handlerNo).replyToChallenge(username, name, instruction, numPrompts, prompt, echo, lastError)); + return adjustNull(getHandler(UUID.fromString(token)).replyToChallenge(username, name, instruction, numPrompts, prompt, echo, lastError)); } @Override - public String askPassword(int handlerNo, String username, boolean resetPassword, String lastError) { - return adjustNull(getHandler(handlerNo).askPassword(username, resetPassword, lastError)); + public String askPassword(String token, String username, boolean resetPassword, String lastError) { + return adjustNull(getHandler(UUID.fromString(token)).askPassword(username, resetPassword, lastError)); } @Override - public String setLastSuccessful(int handlerNo, String userName, String method, String error) { - getHandler(handlerNo).setLastSuccessful(userName, method, error); + public String setLastSuccessful(String token, String userName, String method, String error) { + getHandler(UUID.fromString(token)).setLastSuccessful(userName, method, error); return ""; } @Override - public String getLastSuccessful(int handlerNo, String userName) { - return getHandler(handlerNo).getLastSuccessful(userName); + public String getLastSuccessful(String token, String userName) { + return getHandler(UUID.fromString(token)).getLastSuccessful(userName); } /** From 6ed814b2a510fa20fe86eb568aa6efd9b5dc03fa Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Tue, 19 Apr 2016 11:06:45 +0200 Subject: [PATCH 30/67] add shtm/stm as html files --- .../src/org/jetbrains/builtInWebServer/StaticFileHandler.kt | 2 +- platform/platform-resources/src/META-INF/mime.types | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt index 2158b67948d6..8ea5c96a3ffb 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt @@ -18,7 +18,7 @@ import java.nio.file.Path import java.nio.file.Paths private class StaticFileHandler : WebServerFileHandler() { - override val pageFileExtensions = arrayOf("html", "htm", "shtml") + override val pageFileExtensions = arrayOf("html", "htm", "shtml", "stm", "shtm") private var ssiProcessor: SsiProcessor? = null diff --git a/platform/platform-resources/src/META-INF/mime.types b/platform/platform-resources/src/META-INF/mime.types index 3ae07a484937..1dbfd66428f9 100644 --- a/platform/platform-resources/src/META-INF/mime.types +++ b/platform/platform-resources/src/META-INF/mime.types @@ -101,7 +101,7 @@ application/xslt+xml xsl application/zip zip text/css css - text/html html htm shtml + text/html html htm shtml stm shtm text/mathml mml; text/plain txt text/vcard vcard vcf From 1a5b1228b466a125711f7079755c673760a73ca1 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Tue, 19 Apr 2016 15:40:03 +0200 Subject: [PATCH 31/67] restore broken / redirection functionality --- .../builtInWebServer/BuiltInWebServer.kt | 11 ++++------ .../DefaultWebServerPathHandler.kt | 21 +++++++++++++------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index a754c1dcb5c6..2dd2b3f14948 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -33,7 +33,9 @@ import com.intellij.util.net.NetUtils import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.* import org.jetbrains.ide.HttpRequestHandler -import org.jetbrains.io.* +import org.jetbrains.io.host +import org.jetbrains.io.isLocalOrigin +import org.jetbrains.io.send import java.io.IOException import java.net.InetAddress import java.nio.file.Path @@ -142,11 +144,6 @@ private fun doProcess(request: FullHttpRequest, context: ChannelHandlerContext, return true } - if (request.origin == null && request.referrer == null && request.isRegularBrowser() && !canBeAccessedDirectly(path)) { - HttpResponseStatus.NOT_FOUND.send(context.channel(), request) - return true - } - for (pathHandler in WebServerPathHandler.EP_NAME.extensions) { LOG.catchAndLog { if (pathHandler.process(path, project, request, context, projectName, decodedPath, isCustomHost)) { @@ -249,7 +246,7 @@ internal fun isOwnHostName(host: String): Boolean { } } -private fun canBeAccessedDirectly(path: String): Boolean { +internal fun canBeAccessedDirectly(path: String): Boolean { for (fileHandler in WebServerFileHandler.EP_NAME.extensions) { for (ext in fileHandler.pageFileExtensions) { if (FileUtilRt.extensionEquals(path, ext)) { diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt index a0bf723a0018..f40186f447c7 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt @@ -29,6 +29,9 @@ import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.FullHttpRequest import io.netty.handler.codec.http.HttpRequest import io.netty.handler.codec.http.HttpResponseStatus +import org.jetbrains.io.isRegularBrowser +import org.jetbrains.io.origin +import org.jetbrains.io.referrer import org.jetbrains.io.send import java.nio.file.Path import java.nio.file.Paths @@ -61,11 +64,6 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { var indexUsed = false if (pathInfo.isDirectory()) { - if (!endsWithSlash(decodedRawPath)) { - redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path") - return true - } - var indexVirtualFile: VirtualFile? = null var indexFile: Path? = null if (pathInfo.file == null) { @@ -76,7 +74,13 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { } if (indexFile == null && indexVirtualFile == null) { - HttpResponseStatus.NOT_FOUND.send(channel, request, "Index file doesn't exist.") + HttpResponseStatus.NOT_FOUND.send(channel, request) + return true + } + + // we must redirect only after index file check to not expose directory status + if (!endsWithSlash(decodedRawPath)) { + redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path") return true } @@ -85,6 +89,11 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { pathToFileManager.pathToInfoCache.put(path, pathInfo) } + if (request.origin == null && request.referrer == null && request.isRegularBrowser() && !canBeAccessedDirectly(pathInfo.name)) { + HttpResponseStatus.NOT_FOUND.send(context.channel(), request) + return true + } + if (!indexUsed && !endsWithName(path, pathInfo.name)) { if (endsWithSlash(decodedRawPath)) { indexUsed = true From 5ab012b1d9c02d00c2bf23f228b73c3c7bbca027 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Wed, 20 Apr 2016 14:25:54 +0200 Subject: [PATCH 32/67] rest-api returns 200, built-in web server 404 --- .../DefaultWebServerPathHandler.kt | 12 +++++------- .../builtInWebServer/StaticFileHandler.kt | 10 +++++----- .../src/org/jetbrains/ide/OpenFileHttpService.kt | 15 +++++++++++---- .../src/org/jetbrains/io/Responses.kt | 2 +- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt index f40186f447c7..5f2fefc0950a 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt @@ -29,10 +29,7 @@ import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.FullHttpRequest import io.netty.handler.codec.http.HttpRequest import io.netty.handler.codec.http.HttpResponseStatus -import org.jetbrains.io.isRegularBrowser -import org.jetbrains.io.origin -import org.jetbrains.io.referrer -import org.jetbrains.io.send +import org.jetbrains.io.* import java.nio.file.Path import java.nio.file.Paths @@ -128,15 +125,16 @@ private fun checkAccess(pathInfo: PathInfo, channel: Channel, request: HttpReque if (pathInfo.ioFile != null || pathInfo.file!!.isInLocalFileSystem) { val file = pathInfo.ioFile ?: Paths.get(pathInfo.file!!.path) if (file.isDirectory()) { - HttpResponseStatus.NOT_FOUND.send(channel, request) + HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request) return false } - else if (!checkAccess(channel, file, request, Paths.get(pathInfo.root.path))) { + else if (!checkAccess(file, Paths.get(pathInfo.root.path))) { + HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request) return false } } else if (pathInfo.file!!.`is`(VFileProperty.HIDDEN)) { - HttpResponseStatus.NOT_FOUND.send(channel, request) + HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request) return false } diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt index 8ea5c96a3ffb..bd458b234473 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt @@ -5,14 +5,15 @@ import com.intellij.util.PathUtilRt import io.netty.buffer.ByteBufUtf8Writer import io.netty.channel.Channel import io.netty.channel.ChannelFutureListener -import io.netty.handler.codec.http.* +import io.netty.handler.codec.http.FullHttpRequest +import io.netty.handler.codec.http.HttpMethod +import io.netty.handler.codec.http.HttpUtil +import io.netty.handler.codec.http.LastHttpContent import io.netty.handler.stream.ChunkedStream import org.jetbrains.builtInWebServer.ssi.SsiExternalResolver import org.jetbrains.builtInWebServer.ssi.SsiProcessor import org.jetbrains.io.FileResponses import org.jetbrains.io.addKeepAliveIfNeed -import org.jetbrains.io.okInSafeMode -import org.jetbrains.io.send import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -94,11 +95,10 @@ private class StaticFileHandler : WebServerFileHandler() { } } -internal fun checkAccess(channel: Channel, file: Path, request: HttpRequest, root: Path = file.root): Boolean { +internal fun checkAccess(file: Path, root: Path = file.root): Boolean { var parent = file do { if (!hasAccess(parent)) { - HttpResponseStatus.FORBIDDEN.okInSafeMode().send(channel, request) return false } parent = parent.parent ?: break diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt index 96ca5601edbb..162ea4033321 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt +++ b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt @@ -42,7 +42,8 @@ import org.jetbrains.concurrency.AsyncPromise import org.jetbrains.concurrency.Promise import org.jetbrains.concurrency.catchError import org.jetbrains.concurrency.rejectedPromise -import org.jetbrains.io.okInSafeMode +import org.jetbrains.io.orInSafeMode +import org.jetbrains.io.send import java.nio.file.Path import java.nio.file.Paths import java.util.concurrent.ConcurrentLinkedQueue @@ -123,7 +124,7 @@ internal class OpenFileHttpService : RestService() { .rejected { if (it === NOT_FOUND) { // don't expose file status - sendStatus(HttpResponseStatus.NOT_FOUND.okInSafeMode(), keepAlive, channel) + sendStatus(HttpResponseStatus.NOT_FOUND.orInSafeMode(HttpResponseStatus.OK), keepAlive, channel) LOG.warn("File ${apiRequest.file} not found") } else { @@ -135,14 +136,20 @@ internal class OpenFileHttpService : RestService() { return null } - fun openFile(request: OpenFileRequest, context: ChannelHandlerContext?, httpRequest: HttpRequest?): Promise? { + internal fun openFile(request: OpenFileRequest, context: ChannelHandlerContext, httpRequest: HttpRequest?): Promise? { val path = FileUtil.expandUserHome(request.file!!) val file = Paths.get(FileUtil.toSystemDependentName(path)) if (file.isAbsolute) { if (!file.exists()) { return rejectedPromise(NOT_FOUND) } - return if (context == null || checkAccess(context.channel(), file, httpRequest!!)) openAbsolutePath(file, request) else null + if (checkAccess(file)) { + return openAbsolutePath(file, request) + } + else { + HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.OK).send(context.channel(), httpRequest) + return null + } } // we don't want to call refresh for each attempt on findFileByRelativePath call, so, we do what ourSaveAndSyncHandlerImpl does on frame activation diff --git a/platform/platform-impl/src/org/jetbrains/io/Responses.kt b/platform/platform-impl/src/org/jetbrains/io/Responses.kt index 4a3bd36e3523..b21cfa40ffd1 100644 --- a/platform/platform-impl/src/org/jetbrains/io/Responses.kt +++ b/platform/platform-impl/src/org/jetbrains/io/Responses.kt @@ -126,7 +126,7 @@ fun HttpResponseStatus.send(channel: Channel, request: HttpRequest? = null, desc createStatusResponse(this, request, description).send(channel, request) } -fun HttpResponseStatus.okInSafeMode() = if (ApplicationManager.getApplication()?.isUnitTestMode ?: false) this else HttpResponseStatus.OK +fun HttpResponseStatus.orInSafeMode(safeStatus: HttpResponseStatus) = if (ApplicationManager.getApplication()?.isUnitTestMode ?: false) this else safeStatus private fun createStatusResponse(responseStatus: HttpResponseStatus, request: HttpRequest?, description: String?): HttpResponse { if (request != null && request.method() === HttpMethod.HEAD) { From a11b9a9ce5c0ad5e11630949f7d3e8a72e387fc9 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Thu, 21 Apr 2016 10:53:02 +0200 Subject: [PATCH 33/67] ask the user to confirm if he wants to open the file --- .../org/jetbrains/ide/OpenFileHttpService.kt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt index 162ea4033321..d924544ab8b2 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt +++ b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt @@ -48,6 +48,7 @@ import java.nio.file.Path import java.nio.file.Paths import java.util.concurrent.ConcurrentLinkedQueue import java.util.regex.Pattern +import javax.swing.SwingUtilities private val NOT_FOUND = Promise.createError("not found") private val LINE_AND_COLUMN = Pattern.compile("^(.*?)(?::(\\d+))?(?::(\\d+))?$") @@ -137,13 +138,22 @@ internal class OpenFileHttpService : RestService() { } internal fun openFile(request: OpenFileRequest, context: ChannelHandlerContext, httpRequest: HttpRequest?): Promise? { - val path = FileUtil.expandUserHome(request.file!!) - val file = Paths.get(FileUtil.toSystemDependentName(path)) + val systemIndependentPath = FileUtil.toSystemIndependentName(FileUtil.expandUserHome(request.file!!)) + val file = Paths.get(FileUtil.toSystemDependentName(systemIndependentPath)) if (file.isAbsolute) { if (!file.exists()) { return rejectedPromise(NOT_FOUND) } - if (checkAccess(file)) { + + var isAllowed = checkAccess(file) + if (isAllowed && com.intellij.ide.impl.ProjectUtil.isRemotePath(systemIndependentPath)) { + // invokeAndWait is added to avoid processing many requests in this place: e.g. to prevent abuse of opening many remote files + SwingUtilities.invokeAndWait { + isAllowed = com.intellij.ide.impl.ProjectUtil.confirmLoadingFromRemotePath(systemIndependentPath, "warning.load.file.from.share", "title.load.file.from.share") + } + } + + if (isAllowed) { return openAbsolutePath(file, request) } else { @@ -155,7 +165,7 @@ internal class OpenFileHttpService : RestService() { // we don't want to call refresh for each attempt on findFileByRelativePath call, so, we do what ourSaveAndSyncHandlerImpl does on frame activation val queue = RefreshQueue.getInstance() queue.cancelSession(refreshSessionId) - val mainTask = OpenFileTask(FileUtil.toCanonicalPath(FileUtil.toSystemIndependentName(path), '/'), request) + val mainTask = OpenFileTask(FileUtil.toCanonicalPath(systemIndependentPath, '/'), request) requests.offer(mainTask) val session = queue.createSession(true, true, { while (true) { From f0aca939329abf1f23d35c2fb7649e31100b3861 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Thu, 21 Apr 2016 13:49:07 +0200 Subject: [PATCH 34/67] rest api: limit number of requests per minite and check is host trusted --- .../src/org/jetbrains/ide/RestService.java | 77 ++++++++++++++++++- .../com/intellij/ide/impl/ProjectUtil.java | 9 ++- .../src/org/jetbrains/io/Responses.kt | 10 ++- .../src/org/jetbrains/io/netty.kt | 1 - .../src/messages/IdeBundle.properties | 5 +- .../util/resources/misc/registry.properties | 5 +- 6 files changed, 99 insertions(+), 8 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/ide/RestService.java b/platform/built-in-server/src/org/jetbrains/ide/RestService.java index 3ea42c11956a..e29d40851114 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/RestService.java +++ b/platform/built-in-server/src/org/jetbrains/ide/RestService.java @@ -15,23 +15,33 @@ */ package org.jetbrains.ide; +import com.google.common.base.Supplier; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import com.google.gson.stream.MalformedJsonException; +import com.intellij.ide.IdeBundle; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.util.NotNullLazyValue; +import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream; +import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.util.text.StringUtilRt; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.openapi.wm.IdeFrame; import com.intellij.util.ExceptionUtil; +import com.intellij.util.ObjectUtils; import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.net.NetUtils; import io.netty.buffer.ByteBufInputStream; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; @@ -39,14 +49,25 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.io.NettyKt; import org.jetbrains.io.Responses; +import javax.swing.*; import java.awt.*; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.lang.reflect.InvocationTargetException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.intellij.ide.impl.ProjectUtil.showYesNoDialog; /** * Document your service using apiDoc. To extract big example from source code, consider to use *.coffee file near your source file. @@ -68,6 +89,12 @@ public abstract class RestService extends HttpRequestHandler { } }; + private final LoadingCache abuseCounter = + CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build(CacheLoader.from((Supplier)AtomicInteger::new)); + + private final Cache trustedOrigins = + CacheBuilder.newBuilder().maximumSize(1024).expireAfterWrite(1, TimeUnit.DAYS).build(); + @Override public final boolean isSupported(@NotNull FullHttpRequest request) { if (!isMethodSupported(request.method())) { @@ -115,6 +142,17 @@ public abstract class RestService extends HttpRequestHandler { @Override public final boolean process(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException { try { + if (!isHostTrusted(request)) { + Responses.send(Responses.orInSafeMode(HttpResponseStatus.FORBIDDEN, HttpResponseStatus.OK), context.channel(), request); + return true; + } + + AtomicInteger counter = abuseCounter.get(((InetSocketAddress)context.channel().remoteAddress()).getAddress()); + if (counter.incrementAndGet() > Registry.intValue("ide.rest.api.requests.per.minute", 60)) { + Responses.send(Responses.orInSafeMode(HttpResponseStatus.TOO_MANY_REQUESTS, HttpResponseStatus.OK), context.channel(), request); + return true; + } + String error = execute(urlDecoder, request, context); if (error != null) { Responses.send(HttpResponseStatus.BAD_REQUEST, context.channel(), request, error); @@ -137,7 +175,44 @@ public abstract class RestService extends HttpRequestHandler { return true; } - protected final void activateLastFocusedFrame() { + private boolean isHostTrusted(@NotNull FullHttpRequest request) throws InterruptedException, InvocationTargetException { + String referrer = NettyKt.getOrigin(request); + if (referrer == null) { + referrer = NettyKt.getReferrer(request); + } + + String host; + try { + host = StringUtil.nullize(referrer == null ? null : new URI(referrer).getHost()); + } + catch (URISyntaxException ignored) { + return false; + } + + Ref isTrusted = Ref.create(); + if (host != null) { + if (NetUtils.isLocalhost(host)) { + isTrusted.set(true); + } + else { + isTrusted.set(trustedOrigins.getIfPresent(host)); + } + } + + if (isTrusted.isNull()) { + SwingUtilities.invokeAndWait(() -> { + isTrusted.set(showYesNoDialog( + IdeBundle.message("warning.use.rest.api", getServiceName(), ObjectUtils.chooseNotNull(host, "unknown host")), + "title.use.rest.api")); + if (host != null) { + trustedOrigins.put(host, isTrusted.get()); + } + }); + } + return isTrusted.get(); + } + + protected static void activateLastFocusedFrame() { IdeFrame frame = IdeFocusManager.getGlobalInstance().getLastFocusedFrame(); if (frame instanceof Window) { ((Window)frame).toFront(); diff --git a/platform/platform-impl/src/com/intellij/ide/impl/ProjectUtil.java b/platform/platform-impl/src/com/intellij/ide/impl/ProjectUtil.java index e2461d2b2d59..2a58f4691e26 100644 --- a/platform/platform-impl/src/com/intellij/ide/impl/ProjectUtil.java +++ b/platform/platform-impl/src/com/intellij/ide/impl/ProjectUtil.java @@ -209,11 +209,14 @@ public class ProjectUtil { public static boolean confirmLoadingFromRemotePath(@NotNull String path, @NotNull @PropertyKey(resourceBundle = IdeBundle.BUNDLE) String msgKey, @NotNull @PropertyKey(resourceBundle = IdeBundle.BUNDLE) String titleKey) { + return showYesNoDialog(IdeBundle.message(msgKey, path), titleKey); + } + + public static boolean showYesNoDialog(@NotNull String message, @NotNull @PropertyKey(resourceBundle = IdeBundle.BUNDLE) String titleKey) { final Window window = getActiveFrameOrWelcomeScreen(); - final String msg = IdeBundle.message(msgKey, path); - final String title = IdeBundle.message(titleKey); final Icon icon = Messages.getWarningIcon(); - final int answer = window == null ? Messages.showYesNoDialog(msg, title, icon) : Messages.showYesNoDialog(window, msg, title, icon); + String title = IdeBundle.message(titleKey); + final int answer = window == null ? Messages.showYesNoDialog(message, title, icon) : Messages.showYesNoDialog(window, message, title, icon); return answer == Messages.YES; } diff --git a/platform/platform-impl/src/org/jetbrains/io/Responses.kt b/platform/platform-impl/src/org/jetbrains/io/Responses.kt index b21cfa40ffd1..533cdea6c2cc 100644 --- a/platform/platform-impl/src/org/jetbrains/io/Responses.kt +++ b/platform/platform-impl/src/org/jetbrains/io/Responses.kt @@ -18,6 +18,7 @@ package org.jetbrains.io import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ex.ApplicationInfoEx +import com.intellij.openapi.util.registry.Registry import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBufAllocator import io.netty.buffer.ByteBufUtil @@ -126,7 +127,14 @@ fun HttpResponseStatus.send(channel: Channel, request: HttpRequest? = null, desc createStatusResponse(this, request, description).send(channel, request) } -fun HttpResponseStatus.orInSafeMode(safeStatus: HttpResponseStatus) = if (ApplicationManager.getApplication()?.isUnitTestMode ?: false) this else safeStatus +fun HttpResponseStatus.orInSafeMode(safeStatus: HttpResponseStatus): HttpResponseStatus { + if (!Registry.`is`("ide.rest.api.paranoid.mode", true) || (ApplicationManager.getApplication()?.isUnitTestMode ?: false)) { + return this + } + else { + return safeStatus + } +} private fun createStatusResponse(responseStatus: HttpResponseStatus, request: HttpRequest?, description: String?): HttpResponse { if (request != null && request.method() === HttpMethod.HEAD) { diff --git a/platform/platform-impl/src/org/jetbrains/io/netty.kt b/platform/platform-impl/src/org/jetbrains/io/netty.kt index fd09296971cd..0d0c87f8d090 100644 --- a/platform/platform-impl/src/org/jetbrains/io/netty.kt +++ b/platform/platform-impl/src/org/jetbrains/io/netty.kt @@ -187,7 +187,6 @@ fun HttpRequest.isRegularBrowser() = userAgent?.startsWith("Mozilla/5.0") ?: fal // forbid POST requests from browser without Origin fun HttpRequest.isWriteFromBrowserWithoutOrigin(): Boolean { - val userAgent = userAgent ?: return false val method = method() return origin.isNullOrEmpty() && isRegularBrowser() && (method == HttpMethod.POST || method == HttpMethod.PATCH || method == HttpMethod.PUT || method == HttpMethod.DELETE) } \ No newline at end of file diff --git a/platform/platform-resources-en/src/messages/IdeBundle.properties b/platform/platform-resources-en/src/messages/IdeBundle.properties index 88607e0eaf70..824424dda7ae 100644 --- a/platform/platform-resources-en/src/messages/IdeBundle.properties +++ b/platform/platform-resources-en/src/messages/IdeBundle.properties @@ -1195,4 +1195,7 @@ edit.custom.settings.confirm=File \n''{0}''\n does not exist. Create? warning.load.project.from.share=You are opening a project from a network share. Do you trust this location?\n{0} title.load.project.from.share=Loading Project From Network warning.load.file.from.share=You are opening a file from a network share. Do you want to continue?\n{0} -title.load.file.from.share=Loading File From Network \ No newline at end of file +title.load.file.from.share=Loading File From Network + +warning.use.rest.api='{0}' API is requested. Do you trust '{1}'? +title.use.rest.api=Using REST API \ No newline at end of file diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index a3d6810581ba..a80847d6c2eb 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -731,4 +731,7 @@ ide.screenreader.autodetect.accessibility=false ide.screenreader.autodetect.accessibility.description=Automatically detect whether accessible context is enabled editor.rainbow.identifiers=false -editor.rainbow.identifiers.description=Rainbow identifiers in editor \ No newline at end of file +editor.rainbow.identifiers.description=Rainbow identifiers in editor + +ide.rest.api.paranoid.mode=true +ide.rest.api.requests.per.minute=60 \ No newline at end of file From e861df08e509664ebe25f783724677255865799c Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Thu, 21 Apr 2016 15:29:14 +0200 Subject: [PATCH 35/67] IDEA-CR-10285 set limit to 60, fix quotes --- .../src/org/jetbrains/ide/RestService.java | 10 +++++----- .../src/messages/IdeBundle.properties | 2 +- platform/util/resources/misc/registry.properties | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/ide/RestService.java b/platform/built-in-server/src/org/jetbrains/ide/RestService.java index e29d40851114..014364aa48e3 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/RestService.java +++ b/platform/built-in-server/src/org/jetbrains/ide/RestService.java @@ -142,14 +142,14 @@ public abstract class RestService extends HttpRequestHandler { @Override public final boolean process(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException { try { - if (!isHostTrusted(request)) { - Responses.send(Responses.orInSafeMode(HttpResponseStatus.FORBIDDEN, HttpResponseStatus.OK), context.channel(), request); + AtomicInteger counter = abuseCounter.get(((InetSocketAddress)context.channel().remoteAddress()).getAddress()); + if (counter.incrementAndGet() > Registry.intValue("ide.rest.api.requests.per.minute", 30)) { + Responses.send(Responses.orInSafeMode(HttpResponseStatus.TOO_MANY_REQUESTS, HttpResponseStatus.OK), context.channel(), request); return true; } - AtomicInteger counter = abuseCounter.get(((InetSocketAddress)context.channel().remoteAddress()).getAddress()); - if (counter.incrementAndGet() > Registry.intValue("ide.rest.api.requests.per.minute", 60)) { - Responses.send(Responses.orInSafeMode(HttpResponseStatus.TOO_MANY_REQUESTS, HttpResponseStatus.OK), context.channel(), request); + if (!isHostTrusted(request)) { + Responses.send(Responses.orInSafeMode(HttpResponseStatus.FORBIDDEN, HttpResponseStatus.OK), context.channel(), request); return true; } diff --git a/platform/platform-resources-en/src/messages/IdeBundle.properties b/platform/platform-resources-en/src/messages/IdeBundle.properties index 824424dda7ae..d3f826be420a 100644 --- a/platform/platform-resources-en/src/messages/IdeBundle.properties +++ b/platform/platform-resources-en/src/messages/IdeBundle.properties @@ -1197,5 +1197,5 @@ title.load.project.from.share=Loading Project From Network warning.load.file.from.share=You are opening a file from a network share. Do you want to continue?\n{0} title.load.file.from.share=Loading File From Network -warning.use.rest.api='{0}' API is requested. Do you trust '{1}'? +warning.use.rest.api=''{0}'' API is requested. Do you trust ''{1}''? title.use.rest.api=Using REST API \ No newline at end of file diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index a80847d6c2eb..d21da51552b6 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -734,4 +734,4 @@ editor.rainbow.identifiers=false editor.rainbow.identifiers.description=Rainbow identifiers in editor ide.rest.api.paranoid.mode=true -ide.rest.api.requests.per.minute=60 \ No newline at end of file +ide.rest.api.requests.per.minute=30 \ No newline at end of file From 1c4c39a782e2e973d311b3af65c6fb2b084d1265 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 22 Apr 2016 14:53:05 +0200 Subject: [PATCH 36/67] =?UTF-8?q?built-in=20web=20server=20=E2=80=94=20for?= =?UTF-8?q?bid=20untrusted=20access?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BuiltInWebBrowserUrlProvider.java | 9 +- .../builtInWebServer/BuiltInWebServer.kt | 108 ++++++++++++++++-- .../com/intellij/ide/impl/ProjectUtil.java | 2 +- .../src/com/intellij/util/Urls.java | 7 +- 4 files changed, 111 insertions(+), 15 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java index 1fa74d36be45..1f5b2bffe4bf 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java @@ -53,19 +53,20 @@ public class BuiltInWebBrowserUrlProvider extends WebBrowserUrlProvider implemen String path = info.getPath(); String authority = currentAuthority == null ? "localhost:" + effectiveBuiltInServerPort : currentAuthority; - List urls = new SmartList<>(Urls.newHttpUrl(authority, '/' + project.getName() + '/' + path)); + String query = "?" + BuiltInWebServerKt.TOKEN_PARAM_NAME + "=" + BuiltInWebServerKt.acquireToken(); + List urls = new SmartList<>(Urls.newHttpUrl(authority, '/' + project.getName() + '/' + path, query)); String path2 = info.getRootLessPathIfPossible(); if (path2 != null) { - urls.add(Urls.newHttpUrl(authority, '/' + project.getName() + '/' + path2)); + urls.add(Urls.newHttpUrl(authority, '/' + project.getName() + '/' + path2, query)); } int defaultPort = BuiltInServerManager.getInstance().getPort(); if (currentAuthority == null && defaultPort != effectiveBuiltInServerPort) { String defaultAuthority = "localhost:" + defaultPort; - urls.add(Urls.newHttpUrl(defaultAuthority, '/' + project.getName() + '/' + path)); + urls.add(Urls.newHttpUrl(defaultAuthority, '/' + project.getName() + '/' + path, query)); if (path2 != null) { - urls.add(Urls.newHttpUrl(defaultAuthority, '/' + project.getName() + '/' + path2)); + urls.add(Urls.newHttpUrl(defaultAuthority, '/' + project.getName() + '/' + path2, query)); } } diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index 2dd2b3f14948..d95e84877fe1 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -15,30 +15,41 @@ */ package org.jetbrains.builtInWebServer +import com.google.common.cache.CacheBuilder import com.google.common.net.InetAddresses +import com.intellij.ide.impl.ProjectUtil +import com.intellij.openapi.application.ApplicationNamesInfo +import com.intellij.openapi.application.PathManager import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.catchAndLog import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.SystemInfoRt import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.FileUtilRt import com.intellij.openapi.util.io.endsWithName +import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.VirtualFile -import com.intellij.util.UriUtil import com.intellij.util.directoryStreamIfExists import com.intellij.util.io.URLUtil import com.intellij.util.isDirectory import com.intellij.util.net.NetUtils +import io.netty.channel.Channel import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.* +import io.netty.handler.codec.http.cookie.ClientCookieEncoder +import io.netty.handler.codec.http.cookie.DefaultCookie +import io.netty.handler.codec.http.cookie.ServerCookieDecoder import org.jetbrains.ide.HttpRequestHandler -import org.jetbrains.io.host -import org.jetbrains.io.isLocalOrigin -import org.jetbrains.io.send +import org.jetbrains.io.* +import java.io.File import java.io.IOException import java.net.InetAddress import java.nio.file.Path +import java.util.* +import java.util.concurrent.TimeUnit +import javax.swing.SwingUtilities internal val LOG = Logger.getInstance(BuiltInWebServer::class.java) @@ -73,12 +84,52 @@ class BuiltInWebServer : HttpRequestHandler() { else { projectName = host } - return doProcess(request, context, projectName) + return doProcess(urlDecoder, request, context, projectName) } } -private fun doProcess(request: FullHttpRequest, context: ChannelHandlerContext, projectNameAsHost: String?): Boolean { - val decodedPath = URLUtil.unescapePercentSequences(UriUtil.trimParameters(request.uri())) +const val TOKEN_PARAM_NAME = "__ij-st" + +private val STANDARD_COOKIE by lazy { + val productName = ApplicationNamesInfo.getInstance().lowercaseProductName + val configPath = PathManager.getConfigPath() + val cookieName = productName + "-" + Integer.toHexString(configPath.hashCode()) + val file = File(configPath, cookieName) + var token: String? = null + if (file.exists()) { + try { + token = UUID.fromString(FileUtil.loadFile(file)).toString() + } + catch (e: Exception) { + LOG.warn(e) + } + } + if (token == null) { + token = UUID.randomUUID().toString() + FileUtil.writeToFile(file, token!!) + } + + val cookie = DefaultCookie(cookieName, token!!) + cookie.isHttpOnly = true + cookie.setMaxAge(TimeUnit.DAYS.toMillis(365 * 10)) + cookie.setPath("/") + cookie +} + +// expire after access because we reuse tokens +private val tokens = CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.MINUTES).build() + +internal fun acquireToken(): String { + var token = tokens.asMap().keys.firstOrNull() + if (token == null) { + token = UUID.randomUUID().toString() + tokens.put(token, java.lang.Boolean.TRUE) + } + return token +} + +private fun doProcess(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext, projectNameAsHost: String?): Boolean { + val decodedPath = URLUtil.unescapePercentSequences(urlDecoder.path()) var offset: Int var isEmptyPath: Boolean val isCustomHost = projectNameAsHost != null @@ -139,8 +190,11 @@ private fun doProcess(request: FullHttpRequest, context: ChannelHandlerContext, val path = toIdeaPath(decodedPath, offset) if (path == null) { - LOG.warn("$decodedPath is not valid") - HttpResponseStatus.NOT_FOUND.send(context.channel(), request) + HttpResponseStatus.BAD_REQUEST.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(context.channel(), request) + return true + } + + if (!validateToken(request, context.channel(), urlDecoder)) { return true } @@ -154,6 +208,42 @@ private fun doProcess(request: FullHttpRequest, context: ChannelHandlerContext, return false } +private fun validateToken(request: HttpRequest, channel: Channel, urlDecoder: QueryStringDecoder): Boolean { + val cookieString = request.headers().get(HttpHeaderNames.COOKIE) + if (cookieString != null) { + val cookies = ServerCookieDecoder.STRICT.decode(cookieString) + for (cookie in cookies) { + if (cookie.name() == STANDARD_COOKIE.name()) { + if (cookie.value() == STANDARD_COOKIE.value()) { + return true + } + break + } + } + } + + val token = urlDecoder.parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() + val url = "${channel.uriScheme}://${request.host!!}${urlDecoder.path()}" + if (token != null && tokens.getIfPresent(token) != null) { + tokens.invalidate(token) + // we redirect because it is not easy to change and maintain all places where we send response + val response = HttpResponseStatus.TEMPORARY_REDIRECT.response() + response.headers().add(HttpHeaderNames.LOCATION, url) + response.headers().set(HttpHeaderNames.SET_COOKIE, ClientCookieEncoder.STRICT.encode(STANDARD_COOKIE)) + response.send(channel, request) + return true + } + + SwingUtilities.invokeAndWait { + ProjectUtil.focusProjectWindow(null, true) + Messages.showMessageDialog(ProjectUtil.getActiveFrameOrWelcomeScreen(), "Page '" + StringUtil.trimMiddle(url, 50) + "' requested without authorization, " + + "\nplease open this link to trust it.", "", Messages.getWarningIcon()) + } + + HttpResponseStatus.UNAUTHORIZED.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request) + return false +} + private fun toIdeaPath(decodedPath: String, offset: Int): String? { // must be absolute path (relative to DOCUMENT_ROOT, i.e. scheme://authority/) to properly canonicalize val path = decodedPath.substring(offset) diff --git a/platform/platform-impl/src/com/intellij/ide/impl/ProjectUtil.java b/platform/platform-impl/src/com/intellij/ide/impl/ProjectUtil.java index 2a58f4691e26..4340318b9f69 100644 --- a/platform/platform-impl/src/com/intellij/ide/impl/ProjectUtil.java +++ b/platform/platform-impl/src/com/intellij/ide/impl/ProjectUtil.java @@ -220,7 +220,7 @@ public class ProjectUtil { return answer == Messages.YES; } - private static Window getActiveFrameOrWelcomeScreen() { + public static Window getActiveFrameOrWelcomeScreen() { Window window = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow(); if (window != null) return window; diff --git a/platform/platform-impl/src/com/intellij/util/Urls.java b/platform/platform-impl/src/com/intellij/util/Urls.java index 5ae9a7c7117c..c487ab38c41e 100644 --- a/platform/platform-impl/src/com/intellij/util/Urls.java +++ b/platform/platform-impl/src/com/intellij/util/Urls.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2015 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,6 +72,11 @@ public final class Urls { return newUrl("http", authority, path); } + @NotNull + public static Url newHttpUrl(@NotNull String authority, @Nullable String path, @Nullable String parameters) { + return new UrlImpl("http", authority, path, parameters); + } + @NotNull public static Url newUrl(@NotNull String scheme, @NotNull String authority, @Nullable String path) { return new UrlImpl(scheme, authority, path); From a2cbeb555a7edd52f6ee5a854aa5600dfc59c38d Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 22 Apr 2016 15:33:26 +0200 Subject: [PATCH 37/67] cleanup --- .../src/com/intellij/openapi/ui/MessageDialogBuilder.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/platform/platform-api/src/com/intellij/openapi/ui/MessageDialogBuilder.java b/platform/platform-api/src/com/intellij/openapi/ui/MessageDialogBuilder.java index e6a2e956947e..445027fec026 100644 --- a/platform/platform-api/src/com/intellij/openapi/ui/MessageDialogBuilder.java +++ b/platform/platform-api/src/com/intellij/openapi/ui/MessageDialogBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2014 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -102,10 +102,11 @@ public abstract class MessageDialogBuilder { if (Messages.canShowMacSheetPanel() && !Messages.isApplicationInUnitTestOrHeadless()) { return MacMessages.getInstance().showYesNoDialog(myTitle, myMessage, yesText, noText, WindowManager.getInstance().suggestParentWindow(myProject), myDoNotAskOption); } - } catch (Exception ignored) {} + } + catch (Exception ignored) { + } return Messages.showDialog(myProject, myMessage, myTitle, new String[]{yesText, noText}, 0, myIcon, myDoNotAskOption) == 0 ? Messages.YES : Messages.NO; - } public boolean is() { From 6b2ffe1234df3a0a1425ea49c5ee3e1f8df9842e Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 22 Apr 2016 15:51:06 +0200 Subject: [PATCH 38/67] =?UTF-8?q?built-in=20web=20server=20=E2=80=94=20for?= =?UTF-8?q?bid=20untrusted=20access?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jetbrains/builtInWebServer/BuiltInWebServer.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index d95e84877fe1..16e478a69ec8 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -22,8 +22,10 @@ import com.intellij.openapi.application.ApplicationNamesInfo import com.intellij.openapi.application.PathManager import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.catchAndLog +import com.intellij.openapi.ide.CopyPasteManager import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.ui.MessageDialogBuilder import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.SystemInfoRt import com.intellij.openapi.util.io.FileUtil @@ -43,6 +45,7 @@ import io.netty.handler.codec.http.cookie.DefaultCookie import io.netty.handler.codec.http.cookie.ServerCookieDecoder import org.jetbrains.ide.HttpRequestHandler import org.jetbrains.io.* +import java.awt.datatransfer.StringSelection import java.io.File import java.io.IOException import java.net.InetAddress @@ -236,8 +239,15 @@ private fun validateToken(request: HttpRequest, channel: Channel, urlDecoder: Qu SwingUtilities.invokeAndWait { ProjectUtil.focusProjectWindow(null, true) - Messages.showMessageDialog(ProjectUtil.getActiveFrameOrWelcomeScreen(), "Page '" + StringUtil.trimMiddle(url, 50) + "' requested without authorization, " + - "\nplease open this link to trust it.", "", Messages.getWarningIcon()) + + if (MessageDialogBuilder + .yesNo("", "Page '" + StringUtil.trimMiddle(url, 50) + "' requested without authorization, " + + "\nyou can copy URL and open it in browser to trust it.") + .icon(Messages.getWarningIcon()) + .yesText("Copy authorization URL to clipboard") + .show() == Messages.YES) { + CopyPasteManager.getInstance().setContents(StringSelection(url + "?" + TOKEN_PARAM_NAME + "=" + acquireToken())) + } } HttpResponseStatus.UNAUTHORIZED.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request) From f1b552e157a610c600768a83da547ecf24253478 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 22 Apr 2016 17:55:22 +0200 Subject: [PATCH 39/67] =?UTF-8?q?built-in=20web=20server=20=E2=80=94=20for?= =?UTF-8?q?bid=20untrusted=20access:=20check=20param=20in=20referer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jetbrains/builtInWebServer/BuiltInWebServer.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index 16e478a69ec8..355a998cae51 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -40,9 +40,9 @@ import com.intellij.util.net.NetUtils import io.netty.channel.Channel import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.* -import io.netty.handler.codec.http.cookie.ClientCookieEncoder import io.netty.handler.codec.http.cookie.DefaultCookie import io.netty.handler.codec.http.cookie.ServerCookieDecoder +import io.netty.handler.codec.http.cookie.ServerCookieEncoder import org.jetbrains.ide.HttpRequestHandler import org.jetbrains.io.* import java.awt.datatransfer.StringSelection @@ -112,9 +112,11 @@ private val STANDARD_COOKIE by lazy { FileUtil.writeToFile(file, token!!) } + // explicit setting domain cookie on localhost doesn't work for chrome + // http://stackoverflow.com/questions/8134384/chrome-doesnt-create-cookie-for-domain-localhost-in-broken-https val cookie = DefaultCookie(cookieName, token!!) cookie.isHttpOnly = true - cookie.setMaxAge(TimeUnit.DAYS.toMillis(365 * 10)) + cookie.setMaxAge(TimeUnit.DAYS.toSeconds(365 * 10)) cookie.setPath("/") cookie } @@ -225,14 +227,15 @@ private fun validateToken(request: HttpRequest, channel: Channel, urlDecoder: Qu } } - val token = urlDecoder.parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() + // we must check referrer - if html cached, browser will send request without query + val token = urlDecoder.parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() ?: request.referrer?.let { QueryStringDecoder(it).parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() } val url = "${channel.uriScheme}://${request.host!!}${urlDecoder.path()}" if (token != null && tokens.getIfPresent(token) != null) { tokens.invalidate(token) // we redirect because it is not easy to change and maintain all places where we send response val response = HttpResponseStatus.TEMPORARY_REDIRECT.response() response.headers().add(HttpHeaderNames.LOCATION, url) - response.headers().set(HttpHeaderNames.SET_COOKIE, ClientCookieEncoder.STRICT.encode(STANDARD_COOKIE)) + response.headers().set(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(STANDARD_COOKIE)) response.send(channel, request) return true } From ca1544e6a2e97dd9ad6ddb919960a992d06e28cd Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 25 Apr 2016 17:16:35 +0200 Subject: [PATCH 40/67] =?UTF-8?q?built-in=20web=20server=20=E2=80=94=20dir?= =?UTF-8?q?ect=20access=20without=20token,=20fix=20"first=20request=20afte?= =?UTF-8?q?r=20redirect=20doesn't=20have=20cookies",=20SameSite=20strict?= =?UTF-8?q?=20cookie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../builtInWebServer/BuiltInWebServer.kt | 30 ++++++++++--------- .../DefaultWebServerPathHandler.kt | 15 ++++++---- .../builtInWebServer/StaticFileHandler.kt | 17 +++++------ .../builtInWebServer/WebServerFileHandler.kt | 4 ++- .../builtInWebServer/WebServerPathHandler.kt | 14 ++++----- .../src/org/jetbrains/io/FileResponses.kt | 13 ++++---- .../src/org/jetbrains/io/Responses.kt | 18 ++++++----- .../util/resources/misc/registry.properties | 2 +- 8 files changed, 61 insertions(+), 52 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index 355a998cae51..e88f5fa107d1 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -189,7 +189,7 @@ private fun doProcess(urlDecoder: QueryStringDecoder, request: FullHttpRequest, if (isEmptyPath) { // we must redirect "jsdebug" to "jsdebug/" as nginx does, otherwise browser will treat it as a file instead of a directory, so, relative path will not work - redirectToDirectory(request, context.channel(), projectName) + redirectToDirectory(request, context.channel(), projectName, null) return true } @@ -199,10 +199,6 @@ private fun doProcess(urlDecoder: QueryStringDecoder, request: FullHttpRequest, return true } - if (!validateToken(request, context.channel(), urlDecoder)) { - return true - } - for (pathHandler in WebServerPathHandler.EP_NAME.extensions) { LOG.catchAndLog { if (pathHandler.process(path, project, request, context, projectName, decodedPath, isCustomHost)) { @@ -213,31 +209,37 @@ private fun doProcess(urlDecoder: QueryStringDecoder, request: FullHttpRequest, return false } -private fun validateToken(request: HttpRequest, channel: Channel, urlDecoder: QueryStringDecoder): Boolean { +internal fun validateToken(request: HttpRequest, channel: Channel, redirectToSetCookie: Boolean): HttpHeaders? { val cookieString = request.headers().get(HttpHeaderNames.COOKIE) if (cookieString != null) { val cookies = ServerCookieDecoder.STRICT.decode(cookieString) for (cookie in cookies) { if (cookie.name() == STANDARD_COOKIE.name()) { if (cookie.value() == STANDARD_COOKIE.value()) { - return true + return EmptyHttpHeaders.INSTANCE } break } } } + val urlDecoder = QueryStringDecoder(request.uri()) // we must check referrer - if html cached, browser will send request without query val token = urlDecoder.parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() ?: request.referrer?.let { QueryStringDecoder(it).parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() } val url = "${channel.uriScheme}://${request.host!!}${urlDecoder.path()}" if (token != null && tokens.getIfPresent(token) != null) { tokens.invalidate(token) - // we redirect because it is not easy to change and maintain all places where we send response - val response = HttpResponseStatus.TEMPORARY_REDIRECT.response() - response.headers().add(HttpHeaderNames.LOCATION, url) - response.headers().set(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(STANDARD_COOKIE)) - response.send(channel, request) - return true + if (redirectToSetCookie) { + // we redirect because it is not easy to change and maintain all places where we send response + val response = HttpResponseStatus.TEMPORARY_REDIRECT.response(request) + response.headers().add(HttpHeaderNames.LOCATION, url) + response.headers().set(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(STANDARD_COOKIE) + "; SameSite=strict") + response.send(channel, request) + return response.headers() + } + else { + return DefaultHttpHeaders().set(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(STANDARD_COOKIE) + "; SameSite=strict") + } } SwingUtilities.invokeAndWait { @@ -254,7 +256,7 @@ private fun validateToken(request: HttpRequest, channel: Channel, urlDecoder: Qu } HttpResponseStatus.UNAUTHORIZED.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request) - return false + return null } private fun toIdeaPath(decodedPath: String, offset: Int): String? { diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt index 5f2fefc0950a..66a4fe4b3cbc 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt @@ -41,6 +41,8 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { projectName: String, decodedRawPath: String, isCustomHost: Boolean): Boolean { + val extraHttpHeaders = validateToken(request, context.channel(), false) ?: return true + val channel = context.channel() val pathToFileManager = WebServerPathToFileManager.getInstance(project) var pathInfo = pathToFileManager.pathToInfoCache.getIfPresent(path) @@ -48,7 +50,7 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { pathInfo = pathToFileManager.doFindByRelativePath(path) if (pathInfo == null) { if (path.isEmpty()) { - HttpResponseStatus.NOT_FOUND.send(channel, request, "Index file doesn't exist.") + HttpResponseStatus.NOT_FOUND.send(channel, request, "Index file doesn't exist.", extraHttpHeaders) return true } else { @@ -71,13 +73,13 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { } if (indexFile == null && indexVirtualFile == null) { - HttpResponseStatus.NOT_FOUND.send(channel, request) + HttpResponseStatus.NOT_FOUND.send(channel, request, extraHeaders = extraHttpHeaders) return true } // we must redirect only after index file check to not expose directory status if (!endsWithSlash(decodedRawPath)) { - redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path") + redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path", extraHttpHeaders) return true } @@ -86,7 +88,8 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { pathToFileManager.pathToInfoCache.put(path, pathInfo) } - if (request.origin == null && request.referrer == null && request.isRegularBrowser() && !canBeAccessedDirectly(pathInfo.name)) { + // if extraHttpHeaders is not empty, it means that we get request wih token in the query + if (extraHttpHeaders.isEmpty && request.origin == null && request.referrer == null && request.isRegularBrowser() && !canBeAccessedDirectly(pathInfo.name)) { HttpResponseStatus.NOT_FOUND.send(context.channel(), request) return true } @@ -99,7 +102,7 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { // FallbackResource feature in action, /login requested, /index.php retrieved, we must not redirect /login to /login/ val parentPath = getParentPath(pathInfo.path) if (parentPath != null && endsWithName(path, PathUtilRt.getFileName(parentPath))) { - redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path") + redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path", extraHttpHeaders) return true } } @@ -112,7 +115,7 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { val canonicalPath = if (indexUsed) "$path/${pathInfo.name}" else path for (fileHandler in WebServerFileHandler.EP_NAME.extensions) { LOG.catchAndLog { - if (fileHandler.process(pathInfo!!, canonicalPath, project, request, channel, if (isCustomHost) null else projectName)) { + if (fileHandler.process(pathInfo!!, canonicalPath, project, request, channel, if (isCustomHost) null else projectName, extraHttpHeaders)) { return true } } diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt index bd458b234473..457e39c19f5b 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt @@ -5,10 +5,7 @@ import com.intellij.util.PathUtilRt import io.netty.buffer.ByteBufUtf8Writer import io.netty.channel.Channel import io.netty.channel.ChannelFutureListener -import io.netty.handler.codec.http.FullHttpRequest -import io.netty.handler.codec.http.HttpMethod -import io.netty.handler.codec.http.HttpUtil -import io.netty.handler.codec.http.LastHttpContent +import io.netty.handler.codec.http.* import io.netty.handler.stream.ChunkedStream import org.jetbrains.builtInWebServer.ssi.SsiExternalResolver import org.jetbrains.builtInWebServer.ssi.SsiProcessor @@ -23,21 +20,21 @@ private class StaticFileHandler : WebServerFileHandler() { private var ssiProcessor: SsiProcessor? = null - override fun process(pathInfo: PathInfo, canonicalPath: CharSequence, project: Project, request: FullHttpRequest, channel: Channel, projectNameIfNotCustomHost: String?): Boolean { + override fun process(pathInfo: PathInfo, canonicalPath: CharSequence, project: Project, request: FullHttpRequest, channel: Channel, projectNameIfNotCustomHost: String?, extraHeaders: HttpHeaders): Boolean { if (pathInfo.ioFile != null || pathInfo.file!!.isInLocalFileSystem) { val ioFile = pathInfo.ioFile ?: Paths.get(pathInfo.file!!.path) val nameSequence = ioFile.fileName.toString() //noinspection SpellCheckingInspection if (nameSequence.endsWith(".shtml", true) || nameSequence.endsWith(".stm", true) || nameSequence.endsWith(".shtm", true)) { - processSsi(ioFile, PathUtilRt.getParentPath(canonicalPath.toString()), project, request, channel) + processSsi(ioFile, PathUtilRt.getParentPath(canonicalPath.toString()), project, request, channel, extraHeaders) return true } - FileResponses.sendFile(request, channel, ioFile) + FileResponses.sendFile(request, channel, ioFile, extraHeaders) } else { val file = pathInfo.file!! - val response = FileResponses.prepareSend(request, channel, file.timeStamp, file.name) ?: return true + val response = FileResponses.prepareSend(request, channel, file.timeStamp, file.name, extraHeaders) ?: return true val keepAlive = response.addKeepAliveIfNeed(request) if (request.method() != HttpMethod.HEAD) { @@ -59,7 +56,7 @@ private class StaticFileHandler : WebServerFileHandler() { return true } - private fun processSsi(file: Path, path: String, project: Project, request: FullHttpRequest, channel: Channel) { + private fun processSsi(file: Path, path: String, project: Project, request: FullHttpRequest, channel: Channel, extraHeaders: HttpHeaders) { if (ssiProcessor == null) { ssiProcessor = SsiProcessor(false) } @@ -69,7 +66,7 @@ private class StaticFileHandler : WebServerFileHandler() { var releaseBuffer = true try { val lastModified = ssiProcessor!!.process(SsiExternalResolver(project, request, path, file.parent), file, ByteBufUtf8Writer(buffer)) - val response = FileResponses.prepareSend(request, channel, lastModified, file.fileName.toString()) ?: return + val response = FileResponses.prepareSend(request, channel, lastModified, file.fileName.toString(), extraHeaders) ?: return keepAlive = response.addKeepAliveIfNeed(request) if (request.method() != HttpMethod.HEAD) { HttpUtil.setContentLength(response, buffer.readableBytes().toLong()) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerFileHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerFileHandler.kt index 1e823c4861a4..622322337912 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerFileHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerFileHandler.kt @@ -19,6 +19,7 @@ import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project import io.netty.channel.Channel import io.netty.handler.codec.http.FullHttpRequest +import io.netty.handler.codec.http.HttpHeaders abstract class WebServerFileHandler { companion object { @@ -36,7 +37,8 @@ abstract class WebServerFileHandler { project: Project, request: FullHttpRequest, channel: Channel, - projectNameIfNotCustomHost: String?): Boolean + projectNameIfNotCustomHost: String?, + extraHeaders: HttpHeaders): Boolean } fun getRequestPath(canonicalPath: CharSequence, projectNameIfNotCustomHost: String?) = if (projectNameIfNotCustomHost == null) "/$canonicalPath" else "/$projectNameIfNotCustomHost/$canonicalPath" \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerPathHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerPathHandler.kt index 037a64cb5e8f..ad4dfc3fb21f 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerPathHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerPathHandler.kt @@ -20,10 +20,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VfsUtil import io.netty.channel.Channel import io.netty.channel.ChannelHandlerContext -import io.netty.handler.codec.http.FullHttpRequest -import io.netty.handler.codec.http.HttpHeaderNames -import io.netty.handler.codec.http.HttpRequest -import io.netty.handler.codec.http.HttpResponseStatus +import io.netty.handler.codec.http.* import org.jetbrains.io.host import org.jetbrains.io.response import org.jetbrains.io.send @@ -49,9 +46,12 @@ abstract class WebServerPathHandler { isCustomHost: Boolean): Boolean } -fun redirectToDirectory(request: HttpRequest, channel: Channel, path: String) { - val response = HttpResponseStatus.MOVED_PERMANENTLY.response() +internal fun redirectToDirectory(request: HttpRequest, channel: Channel, path: String, extraHttpHeader: HttpHeaders?) { + val response = HttpResponseStatus.MOVED_PERMANENTLY.response(request) val url = VfsUtil.toUri("${channel.uriScheme}://${request.host!!}/$path/")!! response.headers().add(HttpHeaderNames.LOCATION, url.toASCIIString()) - response.send(channel, request) + extraHttpHeader?.let { + + } + response.send(channel, request, extraHttpHeader) } \ No newline at end of file diff --git a/platform/platform-impl/src/org/jetbrains/io/FileResponses.kt b/platform/platform-impl/src/org/jetbrains/io/FileResponses.kt index c00648bdb53d..6c083affd380 100644 --- a/platform/platform-impl/src/org/jetbrains/io/FileResponses.kt +++ b/platform/platform-impl/src/org/jetbrains/io/FileResponses.kt @@ -35,17 +35,17 @@ object FileResponses { return FILE_MIMETYPE_MAP.getContentType(path) } - private fun checkCache(request: HttpRequest, channel: Channel, lastModified: Long): Boolean { + private fun checkCache(request: HttpRequest, channel: Channel, lastModified: Long, extraHeaders: HttpHeaders): Boolean { val ifModified = request.headers().getTimeMillis(HttpHeaderNames.IF_MODIFIED_SINCE) if (ifModified != null && ifModified >= lastModified) { - HttpResponseStatus.NOT_MODIFIED.send(channel, request) + HttpResponseStatus.NOT_MODIFIED.send(channel, request, extraHeaders = extraHeaders) return true } return false } - fun prepareSend(request: HttpRequest, channel: Channel, lastModified: Long, filename: String): HttpResponse? { - if (checkCache(request, channel, lastModified)) { + fun prepareSend(request: HttpRequest, channel: Channel, lastModified: Long, filename: String, extraHeaders: HttpHeaders): HttpResponse? { + if (checkCache(request, channel, lastModified, extraHeaders)) { return null } @@ -54,11 +54,12 @@ object FileResponses { response.addCommonHeaders() response.headers().set(HttpHeaderNames.CACHE_CONTROL, "private, must-revalidate") response.headers().set(HttpHeaderNames.LAST_MODIFIED, Date(lastModified)) + response.headers().add(extraHeaders) return response } - fun sendFile(request: HttpRequest, channel: Channel, file: Path) { - val response = prepareSend(request, channel, Files.getLastModifiedTime(file).toMillis(), file.fileName.toString()) ?: return + fun sendFile(request: HttpRequest, channel: Channel, file: Path, extraHeaders: HttpHeaders = EmptyHttpHeaders.INSTANCE) { + val response = prepareSend(request, channel, Files.getLastModifiedTime(file).toMillis(), file.fileName.toString(), extraHeaders) ?: return val keepAlive = response.addKeepAliveIfNeed(request) diff --git a/platform/platform-impl/src/org/jetbrains/io/Responses.kt b/platform/platform-impl/src/org/jetbrains/io/Responses.kt index 533cdea6c2cc..718f51aef0ff 100644 --- a/platform/platform-impl/src/org/jetbrains/io/Responses.kt +++ b/platform/platform-impl/src/org/jetbrains/io/Responses.kt @@ -33,8 +33,6 @@ import java.util.* private var SERVER_HEADER_VALUE: String? = null -fun HttpResponseStatus.response(): FullHttpResponse = DefaultFullHttpResponse(HttpVersion.HTTP_1_1, this, Unpooled.EMPTY_BUFFER) - fun response(contentType: String?, content: ByteBuf?): FullHttpResponse { val response = if (content == null) DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK) @@ -79,13 +77,17 @@ fun HttpResponse.addServer() { } } -fun HttpResponse.send(channel: Channel, request: HttpRequest?) { +@JvmOverloads +fun HttpResponse.send(channel: Channel, request: HttpRequest?, extraHeaders: HttpHeaders? = null) { if (status() !== HttpResponseStatus.NOT_MODIFIED && !HttpUtil.isContentLengthSet(this)) { HttpUtil.setContentLength(this, (if (this is FullHttpResponse) content().readableBytes() else 0).toLong()) } addCommonHeaders() + extraHeaders?.let { + headers().add(it) + } send(channel, request != null && !addKeepAliveIfNeed(request)) } @@ -122,13 +124,15 @@ fun HttpResponse.send(channel: Channel, close: Boolean) { } } +fun HttpResponseStatus.response(request: HttpRequest? = null, description: String? = null): HttpResponse = createStatusResponse(this, request, description) + @JvmOverloads -fun HttpResponseStatus.send(channel: Channel, request: HttpRequest? = null, description: String? = null) { - createStatusResponse(this, request, description).send(channel, request) +fun HttpResponseStatus.send(channel: Channel, request: HttpRequest? = null, description: String? = null, extraHeaders: HttpHeaders? = null) { + createStatusResponse(this, request, description).send(channel, request, extraHeaders) } fun HttpResponseStatus.orInSafeMode(safeStatus: HttpResponseStatus): HttpResponseStatus { - if (!Registry.`is`("ide.rest.api.paranoid.mode", true) || (ApplicationManager.getApplication()?.isUnitTestMode ?: false)) { + if (!Registry.`is`("ide.http.server.response.actual.status", true) || (ApplicationManager.getApplication()?.isUnitTestMode ?: false)) { return this } else { @@ -138,7 +142,7 @@ fun HttpResponseStatus.orInSafeMode(safeStatus: HttpResponseStatus): HttpRespons private fun createStatusResponse(responseStatus: HttpResponseStatus, request: HttpRequest?, description: String?): HttpResponse { if (request != null && request.method() === HttpMethod.HEAD) { - return responseStatus.response() + return DefaultFullHttpResponse(HttpVersion.HTTP_1_1, responseStatus, Unpooled.EMPTY_BUFFER) } val builder = StringBuilder() diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index d21da51552b6..f60e407dfff3 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -733,5 +733,5 @@ ide.screenreader.autodetect.accessibility.description=Automatically detect wheth editor.rainbow.identifiers=false editor.rainbow.identifiers.description=Rainbow identifiers in editor -ide.rest.api.paranoid.mode=true +ide.http.server.response.actual.status=true ide.rest.api.requests.per.minute=30 \ No newline at end of file From 7869ab6797fef3a0d0757b47fa434fe9dfcd1b4e Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 25 Apr 2016 17:28:50 +0200 Subject: [PATCH 41/67] constant user token name --- .../org/jetbrains/builtInWebServer/BuiltInWebServer.kt | 10 ++++++---- .../openapi/application/ConfigImportHelper.java | 9 ++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index e88f5fa107d1..c2b81b41d379 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -56,6 +56,9 @@ import javax.swing.SwingUtilities internal val LOG = Logger.getInstance(BuiltInWebServer::class.java) +// name is duplicated in the ConfigImportHelper +private const val IDE_TOKEN_FILE = "user.token" + class BuiltInWebServer : HttpRequestHandler() { override fun isAccessible(request: HttpRequest) = request.isLocalOrigin(onlyAnyOrLoopback = false, hostsOnly = true) @@ -91,13 +94,12 @@ class BuiltInWebServer : HttpRequestHandler() { } } -const val TOKEN_PARAM_NAME = "__ij-st" +internal const val TOKEN_PARAM_NAME = "__ij-st" private val STANDARD_COOKIE by lazy { val productName = ApplicationNamesInfo.getInstance().lowercaseProductName val configPath = PathManager.getConfigPath() - val cookieName = productName + "-" + Integer.toHexString(configPath.hashCode()) - val file = File(configPath, cookieName) + val file = File(configPath, IDE_TOKEN_FILE) var token: String? = null if (file.exists()) { try { @@ -114,7 +116,7 @@ private val STANDARD_COOKIE by lazy { // explicit setting domain cookie on localhost doesn't work for chrome // http://stackoverflow.com/questions/8134384/chrome-doesnt-create-cookie-for-domain-localhost-in-broken-https - val cookie = DefaultCookie(cookieName, token!!) + val cookie = DefaultCookie(productName + "-" + Integer.toHexString(configPath.hashCode()), token!!) cookie.isHttpOnly = true cookie.setMaxAge(TimeUnit.DAYS.toSeconds(365 * 10)) cookie.setPath("/") diff --git a/platform/platform-impl/src/com/intellij/openapi/application/ConfigImportHelper.java b/platform/platform-impl/src/com/intellij/openapi/application/ConfigImportHelper.java index 2329e369d9d9..a34689ad76df 100644 --- a/platform/platform-impl/src/com/intellij/openapi/application/ConfigImportHelper.java +++ b/platform/platform-impl/src/com/intellij/openapi/application/ConfigImportHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2015 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -196,6 +196,9 @@ public class ConfigImportHelper { // Copy old plugins. If some of them are incompatible PluginManager will deal with it FileUtil.copyDir(src, dest); + // delete old user token - we must not reuse it + FileUtil.delete(new File(dest, "user.token")); + File oldPluginsDir = new File(src, PLUGINS_PATH); if (!oldPluginsDir.isDirectory() && SystemInfo.isMac) { oldPluginsDir = getSettingsPath(oldInstallationHome, settings, PathManager.PROPERTY_PLUGINS_PATH, @@ -340,14 +343,14 @@ public class ConfigImportHelper { } if (bundle.containsKey(propertyName)) { return bundle.getString(propertyName); - } + } return null; } catch (IOException e) { return null; } } - + final String fileContent = getContent(file); // try to find custom config path From 9449116e4ed14d8ade33ee4119eecd25d858f1d8 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Mon, 25 Apr 2016 17:36:21 +0200 Subject: [PATCH 42/67] ide.http.server.response.actual.status false by default --- platform/platform-impl/src/org/jetbrains/io/Responses.kt | 2 +- platform/util/resources/misc/registry.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/platform/platform-impl/src/org/jetbrains/io/Responses.kt b/platform/platform-impl/src/org/jetbrains/io/Responses.kt index 718f51aef0ff..3230f28b7d7b 100644 --- a/platform/platform-impl/src/org/jetbrains/io/Responses.kt +++ b/platform/platform-impl/src/org/jetbrains/io/Responses.kt @@ -132,7 +132,7 @@ fun HttpResponseStatus.send(channel: Channel, request: HttpRequest? = null, desc } fun HttpResponseStatus.orInSafeMode(safeStatus: HttpResponseStatus): HttpResponseStatus { - if (!Registry.`is`("ide.http.server.response.actual.status", true) || (ApplicationManager.getApplication()?.isUnitTestMode ?: false)) { + if (Registry.`is`("ide.http.server.response.actual.status", true) || (ApplicationManager.getApplication()?.isUnitTestMode ?: false)) { return this } else { diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index f60e407dfff3..424e502522f0 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -733,5 +733,5 @@ ide.screenreader.autodetect.accessibility.description=Automatically detect wheth editor.rainbow.identifiers=false editor.rainbow.identifiers.description=Rainbow identifiers in editor -ide.http.server.response.actual.status=true +ide.http.server.response.actual.status=false ide.rest.api.requests.per.minute=30 \ No newline at end of file From a325ad365903ee5ff8e6dbf636b350da006ee601 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Tue, 26 Apr 2016 10:29:48 +0200 Subject: [PATCH 43/67] =?UTF-8?q?built-in=20web=20server=20=E2=80=94=20use?= =?UTF-8?q?=20own=20file=20to=20store=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt | 2 +- .../com/intellij/openapi/application/ConfigImportHelper.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index c2b81b41d379..39bd6c7dc740 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -57,7 +57,7 @@ import javax.swing.SwingUtilities internal val LOG = Logger.getInstance(BuiltInWebServer::class.java) // name is duplicated in the ConfigImportHelper -private const val IDE_TOKEN_FILE = "user.token" +private const val IDE_TOKEN_FILE = "user.web.token" class BuiltInWebServer : HttpRequestHandler() { override fun isAccessible(request: HttpRequest) = request.isLocalOrigin(onlyAnyOrLoopback = false, hostsOnly = true) diff --git a/platform/platform-impl/src/com/intellij/openapi/application/ConfigImportHelper.java b/platform/platform-impl/src/com/intellij/openapi/application/ConfigImportHelper.java index a34689ad76df..bbcd4bf3ebbc 100644 --- a/platform/platform-impl/src/com/intellij/openapi/application/ConfigImportHelper.java +++ b/platform/platform-impl/src/com/intellij/openapi/application/ConfigImportHelper.java @@ -198,6 +198,7 @@ public class ConfigImportHelper { // delete old user token - we must not reuse it FileUtil.delete(new File(dest, "user.token")); + FileUtil.delete(new File(dest, "user.web.token")); File oldPluginsDir = new File(src, PLUGINS_PATH); if (!oldPluginsDir.isDirectory() && SystemInfo.isMac) { From f49be7a05bd3568bc888be425fa8eae5d19527be Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Tue, 26 Apr 2016 11:38:50 +0200 Subject: [PATCH 44/67] activatable built-in web server (by default false) --- .../BuiltInWebBrowserUrlProvider.java | 5 +++++ .../builtInWebServer/BuiltInWebServer.kt | 16 ++++++++++++++++ platform/util/resources/misc/registry.properties | 3 ++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java index 1f5b2bffe4bf..97c561c0e086 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java @@ -18,6 +18,7 @@ package org.jetbrains.builtInWebServer; import com.intellij.ide.browsers.OpenInBrowserRequest; import com.intellij.ide.browsers.WebBrowserService; import com.intellij.ide.browsers.WebBrowserUrlProvider; +import com.intellij.ide.util.PropertiesComponent; import com.intellij.lang.Language; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.Project; @@ -70,6 +71,10 @@ public class BuiltInWebBrowserUrlProvider extends WebBrowserUrlProvider implemen } } + if (BuiltInWebServerKt.isActivatable()) { + PropertiesComponent.getInstance().setValue("ide.built.in.web.server.active", true); + } + return urls; } diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index 39bd6c7dc740..7185f37c72bd 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -18,6 +18,8 @@ package org.jetbrains.builtInWebServer import com.google.common.cache.CacheBuilder import com.google.common.net.InetAddresses import com.intellij.ide.impl.ProjectUtil +import com.intellij.ide.util.PropertiesComponent +import com.intellij.notification.NotificationType import com.intellij.openapi.application.ApplicationNamesInfo import com.intellij.openapi.application.PathManager import com.intellij.openapi.diagnostic.Logger @@ -31,6 +33,7 @@ import com.intellij.openapi.util.SystemInfoRt import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.FileUtilRt import com.intellij.openapi.util.io.endsWithName +import com.intellij.openapi.util.registry.Registry import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.directoryStreamIfExists @@ -43,8 +46,10 @@ import io.netty.handler.codec.http.* import io.netty.handler.codec.http.cookie.DefaultCookie import io.netty.handler.codec.http.cookie.ServerCookieDecoder import io.netty.handler.codec.http.cookie.ServerCookieEncoder +import org.jetbrains.ide.BuiltInServerManagerImpl import org.jetbrains.ide.HttpRequestHandler import org.jetbrains.io.* +import org.jetbrains.notification.SingletonNotificationManager import java.awt.datatransfer.StringSelection import java.io.File import java.io.IOException @@ -59,6 +64,10 @@ internal val LOG = Logger.getInstance(BuiltInWebServer::class.java) // name is duplicated in the ConfigImportHelper private const val IDE_TOKEN_FILE = "user.web.token" +private val notificationManager by lazy { + SingletonNotificationManager(BuiltInServerManagerImpl.NOTIFICATION_GROUP.getValue(), NotificationType.INFORMATION, null) +} + class BuiltInWebServer : HttpRequestHandler() { override fun isAccessible(request: HttpRequest) = request.isLocalOrigin(onlyAnyOrLoopback = false, hostsOnly = true) @@ -94,6 +103,8 @@ class BuiltInWebServer : HttpRequestHandler() { } } +internal fun isActivatable() = Registry.`is`("ide.built.in.web.server.activatable", false) + internal const val TOKEN_PARAM_NAME = "__ij-st" private val STANDARD_COOKIE by lazy { @@ -189,6 +200,11 @@ private fun doProcess(urlDecoder: QueryStringDecoder, request: FullHttpRequest, return false }) ?: candidateByDirectoryName ?: return false + if (isActivatable() && !PropertiesComponent.getInstance().getBoolean("ide.built.in.web.server.active")) { + notificationManager.notify("Built-in web server is deactivated, to activate, please use Open in Browser", null) + return false + } + if (isEmptyPath) { // we must redirect "jsdebug" to "jsdebug/" as nginx does, otherwise browser will treat it as a file instead of a directory, so, relative path will not work redirectToDirectory(request, context.channel(), projectName, null) diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index 424e502522f0..e497dfe258bf 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -734,4 +734,5 @@ editor.rainbow.identifiers=false editor.rainbow.identifiers.description=Rainbow identifiers in editor ide.http.server.response.actual.status=false -ide.rest.api.requests.per.minute=30 \ No newline at end of file +ide.rest.api.requests.per.minute=30 +ide.built.in.web.server.activatable=false \ No newline at end of file From 74d8f3d799283ec8e6023e921cc40a9a929b0a17 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Tue, 26 Apr 2016 12:55:27 +0200 Subject: [PATCH 45/67] cleanup --- .../org/jetbrains/builtInWebServer/WebServerPathHandler.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerPathHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerPathHandler.kt index ad4dfc3fb21f..772c9c35a6a1 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerPathHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerPathHandler.kt @@ -46,12 +46,9 @@ abstract class WebServerPathHandler { isCustomHost: Boolean): Boolean } -internal fun redirectToDirectory(request: HttpRequest, channel: Channel, path: String, extraHttpHeader: HttpHeaders?) { +internal fun redirectToDirectory(request: HttpRequest, channel: Channel, path: String, extraHeaders: HttpHeaders?) { val response = HttpResponseStatus.MOVED_PERMANENTLY.response(request) val url = VfsUtil.toUri("${channel.uriScheme}://${request.host!!}/$path/")!! response.headers().add(HttpHeaderNames.LOCATION, url.toASCIIString()) - extraHttpHeader?.let { - - } - response.send(channel, request, extraHttpHeader) + response.send(channel, request, extraHeaders) } \ No newline at end of file From 94fc868cf29d523d8015a637efad474f96e69633 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Tue, 26 Apr 2016 15:40:30 +0200 Subject: [PATCH 46/67] cleanup --- .../org/jetbrains/ide/XmlRpcServerImpl.java | 67 +++++++++---------- .../src/org/jetbrains/io/SubServer.java | 3 +- .../src/com/intellij/ide/XmlRpcServer.java | 5 +- .../io/DelegatingHttpRequestHandlerBase.kt | 6 +- .../io/PortUnificationServerHandler.java | 6 +- 5 files changed, 39 insertions(+), 48 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/ide/XmlRpcServerImpl.java b/platform/built-in-server/src/org/jetbrains/ide/XmlRpcServerImpl.java index 782136dd77f9..4ad9a213b925 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/XmlRpcServerImpl.java +++ b/platform/built-in-server/src/org/jetbrains/ide/XmlRpcServerImpl.java @@ -75,48 +75,45 @@ public class XmlRpcServerImpl implements XmlRpcServer { } @Override - public boolean process(@NotNull String path, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context, @Nullable Map handlers) throws IOException { + public boolean process(@NotNull String path, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context, @Nullable Map handlers) { if (!(path.isEmpty() || (path.length() == 1 && path.charAt(0) == '/') || path.equalsIgnoreCase("/rpc2"))) { return false; } - if (request.method() == HttpMethod.POST) { - ByteBuf result; - ByteBuf content = request.content(); - if (content.readableBytes() == 0) { - Responses.send(HttpResponseStatus.BAD_REQUEST, context.channel(), request); - return true; - } + if (request.method() != HttpMethod.POST) { + return false; + } - ByteBufInputStream in = new ByteBufInputStream(content); - try { - XmlRpcServerRequest xmlRpcServerRequest = new XmlRpcRequestProcessor().decodeRequest(in); - if (StringUtil.isEmpty(xmlRpcServerRequest.getMethodName())) { - LOG.warn("method name empty"); - return false; - } - - Object response = invokeHandler(getHandler(xmlRpcServerRequest.getMethodName(), handlers == null ? handlerMapping : handlers), xmlRpcServerRequest); - result = Unpooled.wrappedBuffer(new XmlRpcResponseProcessor().encodeResponse(response, CharsetToolkit.UTF8)); - } - catch (SAXParseException e) { - LOG.warn(e); - Responses.send(HttpResponseStatus.BAD_REQUEST, context.channel(), request); - return true; - } - catch (Throwable e) { - context.channel().close(); - LOG.error(e); - return true; - } - finally { - in.close(); - } - - Responses.send(Responses.response("text/xml", result), context.channel(), request); + ByteBuf content = request.content(); + if (content.readableBytes() == 0) { + Responses.send(HttpResponseStatus.BAD_REQUEST, context.channel(), request); return true; } - return false; + + ByteBuf result; + try (ByteBufInputStream in = new ByteBufInputStream(content)) { + XmlRpcServerRequest xmlRpcServerRequest = new XmlRpcRequestProcessor().decodeRequest(in); + if (StringUtil.isEmpty(xmlRpcServerRequest.getMethodName())) { + LOG.warn("method name empty"); + return false; + } + + Object response = invokeHandler(getHandler(xmlRpcServerRequest.getMethodName(), handlers == null ? handlerMapping : handlers), xmlRpcServerRequest); + result = Unpooled.wrappedBuffer(new XmlRpcResponseProcessor().encodeResponse(response, CharsetToolkit.UTF8)); + } + catch (SAXParseException e) { + LOG.warn(e); + Responses.send(HttpResponseStatus.BAD_REQUEST, context.channel(), request); + return true; + } + catch (Throwable e) { + context.channel().close(); + LOG.error(e); + return true; + } + + Responses.send(Responses.response("text/xml", result), context.channel(), request); + return true; } private static Object getHandler(@NotNull String methodName, @NotNull Map handlers) { diff --git a/platform/built-in-server/src/org/jetbrains/io/SubServer.java b/platform/built-in-server/src/org/jetbrains/io/SubServer.java index c5e58d805f36..67a52d832d21 100644 --- a/platform/built-in-server/src/org/jetbrains/io/SubServer.java +++ b/platform/built-in-server/src/org/jetbrains/io/SubServer.java @@ -31,7 +31,6 @@ import io.netty.handler.codec.http.QueryStringDecoder; import org.jetbrains.annotations.NotNull; import org.jetbrains.ide.CustomPortServerManager; -import java.io.IOException; import java.net.InetSocketAddress; import java.util.Map; @@ -123,7 +122,7 @@ public final class SubServer implements CustomPortServerManager.CustomPortServic } @Override - protected boolean process(@NotNull ChannelHandlerContext context, @NotNull FullHttpRequest request, @NotNull QueryStringDecoder urlDecoder) throws IOException { + protected boolean process(@NotNull ChannelHandlerContext context, @NotNull FullHttpRequest request, @NotNull QueryStringDecoder urlDecoder) { if (handlers.isEmpty()) { // not yet initialized, for example, P2PTransport could add handlers after we bound. return false; diff --git a/platform/platform-api/src/com/intellij/ide/XmlRpcServer.java b/platform/platform-api/src/com/intellij/ide/XmlRpcServer.java index 16dafdcd8775..eed4583c5ab0 100644 --- a/platform/platform-api/src/com/intellij/ide/XmlRpcServer.java +++ b/platform/platform-api/src/com/intellij/ide/XmlRpcServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2013 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import io.netty.handler.codec.http.FullHttpRequest; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.IOException; import java.util.Map; public interface XmlRpcServer { @@ -31,7 +30,7 @@ public interface XmlRpcServer { void removeHandler(String name); - boolean process(@NotNull String path, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context, @Nullable Map handlers) throws IOException; + boolean process(@NotNull String path, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context, @Nullable Map handlers); final class SERVICE { private SERVICE() { diff --git a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandlerBase.kt b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandlerBase.kt index bfe0014ae145..d8c1914c8e39 100644 --- a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandlerBase.kt +++ b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandlerBase.kt @@ -16,6 +16,7 @@ package org.jetbrains.io import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.diagnostic.debug import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.FullHttpRequest import io.netty.handler.codec.http.HttpResponseStatus @@ -23,16 +24,13 @@ import io.netty.handler.codec.http.QueryStringDecoder internal abstract class DelegatingHttpRequestHandlerBase : SimpleChannelInboundHandlerAdapter() { override fun messageReceived(context: ChannelHandlerContext, message: FullHttpRequest) { - if (Logger.getInstance(BuiltInServer::class.java).isDebugEnabled) { - Logger.getInstance(BuiltInServer::class.java).debug("IN HTTP: " + message.uri()) - } + Logger.getInstance(BuiltInServer::class.java).debug { "IN HTTP: $message" } if (!process(context, message, QueryStringDecoder(message.uri()))) { HttpResponseStatus.NOT_FOUND.send(context.channel(), message) } } - @Throws(Exception::class) protected abstract fun process(context: ChannelHandlerContext, request: FullHttpRequest, urlDecoder: QueryStringDecoder): Boolean diff --git a/platform/platform-impl/src/org/jetbrains/io/PortUnificationServerHandler.java b/platform/platform-impl/src/org/jetbrains/io/PortUnificationServerHandler.java index 749487e22890..2ccd269c6fc8 100644 --- a/platform/platform-impl/src/org/jetbrains/io/PortUnificationServerHandler.java +++ b/platform/platform-impl/src/org/jetbrains/io/PortUnificationServerHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2015 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,8 +34,6 @@ import java.security.KeyStore; import java.security.Security; import java.util.UUID; -import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; - @ChannelHandler.Sharable class PortUnificationServerHandler extends Decoder { // keytool -genkey -keyalg RSA -alias selfsigned -keystore cert.jks -storepass jetbrains -validity 10000 -keysize 2048 @@ -114,7 +112,7 @@ class PortUnificationServerHandler extends Decoder { public void write(ChannelHandlerContext context, Object message, ChannelPromise promise) throws Exception { if (message instanceof HttpResponse) { HttpResponse response = (HttpResponse)message; - logger.debug("OUT HTTP: " + response.status().code() + " " + response.headers().getAsString(CONTENT_TYPE)); + logger.debug("OUT HTTP: " + response.toString()); } super.write(context, message, promise); } From 40a1c99000747e3f1feea6fe62a86c8698599be4 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Tue, 26 Apr 2016 15:42:21 +0200 Subject: [PATCH 47/67] use MOVED_PERMANENTLY instead of TEMPORARY_REDIRECT, short temp token --- .../builtInWebServer/BuiltInWebServer.kt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index 7185f37c72bd..c5ea9abecb67 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -53,8 +53,10 @@ import org.jetbrains.notification.SingletonNotificationManager import java.awt.datatransfer.StringSelection import java.io.File import java.io.IOException +import java.math.BigInteger import java.net.InetAddress import java.nio.file.Path +import java.security.SecureRandom import java.util.* import java.util.concurrent.TimeUnit import javax.swing.SwingUtilities @@ -65,7 +67,7 @@ internal val LOG = Logger.getInstance(BuiltInWebServer::class.java) private const val IDE_TOKEN_FILE = "user.web.token" private val notificationManager by lazy { - SingletonNotificationManager(BuiltInServerManagerImpl.NOTIFICATION_GROUP.getValue(), NotificationType.INFORMATION, null) + SingletonNotificationManager(BuiltInServerManagerImpl.NOTIFICATION_GROUP.value, NotificationType.INFORMATION, null) } class BuiltInWebServer : HttpRequestHandler() { @@ -105,7 +107,7 @@ class BuiltInWebServer : HttpRequestHandler() { internal fun isActivatable() = Registry.`is`("ide.built.in.web.server.activatable", false) -internal const val TOKEN_PARAM_NAME = "__ij-st" +internal const val TOKEN_PARAM_NAME = "_ijt" private val STANDARD_COOKIE by lazy { val productName = ApplicationNamesInfo.getInstance().lowercaseProductName @@ -140,12 +142,19 @@ private val tokens = CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.MIN internal fun acquireToken(): String { var token = tokens.asMap().keys.firstOrNull() if (token == null) { - token = UUID.randomUUID().toString() + token = TokenGenerator.generate() tokens.put(token, java.lang.Boolean.TRUE) } return token } +// http://stackoverflow.com/a/41156 - shorter than UUID, but secure +private object TokenGenerator { + private val random = SecureRandom() + + fun generate(): String = BigInteger(130, random).toString(32) +} + private fun doProcess(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext, projectNameAsHost: String?): Boolean { val decodedPath = URLUtil.unescapePercentSequences(urlDecoder.path()) var offset: Int @@ -249,7 +258,7 @@ internal fun validateToken(request: HttpRequest, channel: Channel, redirectToSet tokens.invalidate(token) if (redirectToSetCookie) { // we redirect because it is not easy to change and maintain all places where we send response - val response = HttpResponseStatus.TEMPORARY_REDIRECT.response(request) + val response = HttpResponseStatus.MOVED_PERMANENTLY.response(request) response.headers().add(HttpHeaderNames.LOCATION, url) response.headers().set(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(STANDARD_COOKIE) + "; SameSite=strict") response.send(channel, request) From 69f590c81a0da73b83252a2e4b8cb9f2504f6e55 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Tue, 26 Apr 2016 16:16:35 +0200 Subject: [PATCH 48/67] cleanup --- .../org/jetbrains/io/jsonRpc/ClientManager.kt | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/io/jsonRpc/ClientManager.kt b/platform/built-in-server/src/org/jetbrains/io/jsonRpc/ClientManager.kt index 5bed61f85081..4eb035b48410 100644 --- a/platform/built-in-server/src/org/jetbrains/io/jsonRpc/ClientManager.kt +++ b/platform/built-in-server/src/org/jetbrains/io/jsonRpc/ClientManager.kt @@ -10,22 +10,16 @@ import io.netty.util.AttributeKey import org.jetbrains.concurrency.Promise import org.jetbrains.io.webSocket.WebSocketServerOptions -val CLIENT = AttributeKey.valueOf("SocketHandler.client") +internal val CLIENT = AttributeKey.valueOf("SocketHandler.client") class ClientManager(private val listener: ClientListener?, val exceptionHandler: ExceptionHandler, options: WebSocketServerOptions? = null) : Disposable { - private val heartbeatTimer = SimpleTimer.getInstance().setUp(Runnable { - synchronized (clients) { - if (clients.isEmpty) { - return@Runnable + private val heartbeatTimer = SimpleTimer.getInstance().setUp({ + forEachClient(TObjectProcedure { + if (it.channel.isActive) { + it.sendHeartbeat() } - - clients.forEach { client -> - if (client.channel.isActive) { - client.sendHeartbeat() - } - true - } - } + true + }) }, (options ?: WebSocketServerOptions()).heartbeatDelay.toLong()) private val clients = THashSet() @@ -94,10 +88,6 @@ class ClientManager(private val listener: ClientListener?, val exceptionHandler: fun forEachClient(procedure: TObjectProcedure) { synchronized (clients) { - if (clients.isEmpty) { - return - } - clients.forEach(procedure) } } From 2d15b5d3077f377a43c73cfbf2f057cbcc4651f1 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Tue, 26 Apr 2016 16:20:50 +0200 Subject: [PATCH 49/67] cleanup --- .../src/org/jetbrains/io/fastCgi/FastCgiService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/built-in-server/src/org/jetbrains/io/fastCgi/FastCgiService.kt b/platform/built-in-server/src/org/jetbrains/io/fastCgi/FastCgiService.kt index 75d090042f7f..437c0ea48c8f 100644 --- a/platform/built-in-server/src/org/jetbrains/io/fastCgi/FastCgiService.kt +++ b/platform/built-in-server/src/org/jetbrains/io/fastCgi/FastCgiService.kt @@ -30,7 +30,7 @@ import org.jetbrains.concurrency.doneRun import org.jetbrains.io.* import java.util.concurrent.atomic.AtomicInteger -val LOG: Logger = Logger.getInstance(FastCgiService::class.java) +val LOG = Logger.getInstance(FastCgiService::class.java) // todo send FCGI_ABORT_REQUEST if client channel disconnected abstract class FastCgiService(project: Project) : SingleConnectionNetService(project) { From 21df259d755e4b7deb96004110d07d1b239fc2f7 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Tue, 26 Apr 2016 16:30:01 +0200 Subject: [PATCH 50/67] =?UTF-8?q?dart/php=20handler=20=E2=80=94=20don't=20?= =?UTF-8?q?use=20redirect=20to=20set=20cookie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../builtInWebServer/BuiltInWebServer.kt | 14 ++------- .../DefaultWebServerPathHandler.kt | 2 +- .../jetbrains/io/fastCgi/FastCgiService.kt | 30 ++++++++++--------- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index c5ea9abecb67..0f3d9b2a31a3 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -236,7 +236,7 @@ private fun doProcess(urlDecoder: QueryStringDecoder, request: FullHttpRequest, return false } -internal fun validateToken(request: HttpRequest, channel: Channel, redirectToSetCookie: Boolean): HttpHeaders? { +internal fun validateToken(request: HttpRequest, channel: Channel): HttpHeaders? { val cookieString = request.headers().get(HttpHeaderNames.COOKIE) if (cookieString != null) { val cookies = ServerCookieDecoder.STRICT.decode(cookieString) @@ -256,17 +256,7 @@ internal fun validateToken(request: HttpRequest, channel: Channel, redirectToSet val url = "${channel.uriScheme}://${request.host!!}${urlDecoder.path()}" if (token != null && tokens.getIfPresent(token) != null) { tokens.invalidate(token) - if (redirectToSetCookie) { - // we redirect because it is not easy to change and maintain all places where we send response - val response = HttpResponseStatus.MOVED_PERMANENTLY.response(request) - response.headers().add(HttpHeaderNames.LOCATION, url) - response.headers().set(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(STANDARD_COOKIE) + "; SameSite=strict") - response.send(channel, request) - return response.headers() - } - else { - return DefaultHttpHeaders().set(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(STANDARD_COOKIE) + "; SameSite=strict") - } + return DefaultHttpHeaders().set(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(STANDARD_COOKIE) + "; SameSite=strict") } SwingUtilities.invokeAndWait { diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt index 66a4fe4b3cbc..e88bb2c98193 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt @@ -41,7 +41,7 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { projectName: String, decodedRawPath: String, isCustomHost: Boolean): Boolean { - val extraHttpHeaders = validateToken(request, context.channel(), false) ?: return true + val extraHttpHeaders = validateToken(request, context.channel()) ?: return true val channel = context.channel() val pathToFileManager = WebServerPathToFileManager.getInstance(project) diff --git a/platform/built-in-server/src/org/jetbrains/io/fastCgi/FastCgiService.kt b/platform/built-in-server/src/org/jetbrains/io/fastCgi/FastCgiService.kt index 437c0ea48c8f..a295c1ca12fa 100644 --- a/platform/built-in-server/src/org/jetbrains/io/fastCgi/FastCgiService.kt +++ b/platform/built-in-server/src/org/jetbrains/io/fastCgi/FastCgiService.kt @@ -18,7 +18,6 @@ package org.jetbrains.io.fastCgi import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.util.Consumer -import com.intellij.util.containers.ConcurrentIntObjectMap import com.intellij.util.containers.ContainerUtil import io.netty.bootstrap.Bootstrap import io.netty.buffer.ByteBuf @@ -35,7 +34,7 @@ val LOG = Logger.getInstance(FastCgiService::class.java) // todo send FCGI_ABORT_REQUEST if client channel disconnected abstract class FastCgiService(project: Project) : SingleConnectionNetService(project) { private val requestIdCounter = AtomicInteger() - protected val requests: ConcurrentIntObjectMap = ContainerUtil.createConcurrentIntObjectMap() + private val requests = ContainerUtil.createConcurrentIntObjectMap() override fun configureBootstrap(bootstrap: Bootstrap, errorOutputConsumer: Consumer) { bootstrap.handler { @@ -47,8 +46,8 @@ abstract class FastCgiService(project: Project) : SingleConnectionNetService(pro if (!requests.isEmpty) { val waitingClients = requests.elements().toList() requests.clear() - for (channel in waitingClients) { - sendBadGateway(channel) + for (client in waitingClients) { + sendBadGateway(client.channel, client.extraHeaders) } } } @@ -103,30 +102,30 @@ abstract class FastCgiService(project: Project) : SingleConnectionNetService(pro } } finally { - val channel = requests.remove(fastCgiRequest.requestId) - if (channel != null) { - sendBadGateway(channel) + requests.remove(fastCgiRequest.requestId)?.let { + sendBadGateway(it.channel, it.extraHeaders) } } } - fun allocateRequestId(channel: Channel): Int { + fun allocateRequestId(channel: Channel, extraHeaders: HttpHeaders): Int { var requestId = requestIdCounter.getAndIncrement() if (requestId >= java.lang.Short.MAX_VALUE) { requestIdCounter.set(0) requestId = requestIdCounter.getAndDecrement() } - requests.put(requestId, channel) + requests.put(requestId, ClientInfo(channel, extraHeaders)) return requestId } fun responseReceived(id: Int, buffer: ByteBuf?) { - val channel = requests.remove(id) - if (channel == null || !channel.isActive) { + val client = requests.remove(id) + if (client == null || !client.channel.isActive) { buffer?.release() return } + val channel = client.channel if (buffer == null) { HttpResponseStatus.BAD_GATEWAY.send(channel) return @@ -139,6 +138,7 @@ abstract class FastCgiService(project: Project) : SingleConnectionNetService(pro if (!HttpUtil.isContentLengthSet(httpResponse)) { HttpUtil.setContentLength(httpResponse, buffer.readableBytes().toLong()) } + httpResponse.headers().add(client.extraHeaders) } catch (e: Throwable) { buffer.release() @@ -155,10 +155,10 @@ abstract class FastCgiService(project: Project) : SingleConnectionNetService(pro } } -private fun sendBadGateway(channel: Channel) { +private fun sendBadGateway(channel: Channel, extraHeaders: HttpHeaders) { try { if (channel.isActive) { - HttpResponseStatus.BAD_GATEWAY.send(channel) + HttpResponseStatus.BAD_GATEWAY.send(channel, extraHeaders = extraHeaders) } } catch (e: Throwable) { @@ -218,4 +218,6 @@ private fun parseHeaders(response: HttpResponse, buffer: ByteBuf) { response.headers().add(key, value) } } -} \ No newline at end of file +} + +private class ClientInfo(val channel: Channel, val extraHeaders: HttpHeaders) \ No newline at end of file From 332cdcf97d77dd53821e55e04ca014f8805e2e91 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Wed, 27 Apr 2016 11:29:00 +0200 Subject: [PATCH 51/67] activate built-in web server only on real open invocation --- .../BuiltInWebBrowserUrlProvider.java | 7 +-- .../ide/browsers/BrowserLauncherImpl.java | 62 ++++++++++++++++++- xml/impl/xml.iml | 4 +- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java index 97c561c0e086..61e5db5ca9c3 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java @@ -18,7 +18,6 @@ package org.jetbrains.builtInWebServer; import com.intellij.ide.browsers.OpenInBrowserRequest; import com.intellij.ide.browsers.WebBrowserService; import com.intellij.ide.browsers.WebBrowserUrlProvider; -import com.intellij.ide.util.PropertiesComponent; import com.intellij.lang.Language; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.Project; @@ -70,11 +69,7 @@ public class BuiltInWebBrowserUrlProvider extends WebBrowserUrlProvider implemen urls.add(Urls.newHttpUrl(defaultAuthority, '/' + project.getName() + '/' + path2, query)); } } - - if (BuiltInWebServerKt.isActivatable()) { - PropertiesComponent.getInstance().setValue("ide.built.in.web.server.active", true); - } - + return urls; } diff --git a/xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java b/xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java index 65da7b30722c..7a62abe0b33f 100644 --- a/xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java +++ b/xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2014 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,22 +20,80 @@ import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.util.ExecUtil; import com.intellij.ide.GeneralSettings; import com.intellij.ide.IdeBundle; +import com.intellij.ide.util.PropertiesComponent; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.options.ShowSettingsUtil; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.ui.AppUIUtil; import com.intellij.util.ArrayUtil; +import com.intellij.util.Url; +import com.intellij.util.Urls; +import com.intellij.util.net.NetUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.builtInWebServer.BuiltInServerOptions; +import org.jetbrains.ide.BuiltInServerManager; +import java.net.InetAddress; import java.net.URI; +import java.net.UnknownHostException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -final class BrowserLauncherImpl extends BrowserLauncherAppless { +public final class BrowserLauncherImpl extends BrowserLauncherAppless { + @Override + public void browse(@NotNull String url, @Nullable WebBrowser browser, @Nullable Project project) { + if (Registry.is("ide.built.in.web.server.activatable", false)) { + Url parsedUrl = Urls.parse(url, false); + if (parsedUrl != null && parsedUrl.getAuthority() != null && isOnBuiltInWebServerByAuthority(parsedUrl.getAuthority())) { + PropertiesComponent.getInstance().setValue("ide.built.in.web.server.active", true); + } + } + + super.browse(url, browser, project); + } + + public static boolean isOnBuiltInWebServerByAuthority(@NotNull String authority) { + int portIndex = authority.indexOf(':'); + if (portIndex < 0 || portIndex == authority.length() - 1) { + return false; + } + + int port; + try { + port = Integer.parseInt(authority.substring(portIndex + 1)); + } + catch (NumberFormatException ignored) { + return false; + } + + if (BuiltInServerOptions.getInstance().builtInServerPort != port && BuiltInServerManager.getInstance().getPort() != port) { + return false; + } + + String host = authority.substring(0, portIndex); + if (NetUtils.isLocalhost(host)) { + return true; + } + + InetAddress inetAddress; + try { + inetAddress = InetAddress.getByName(host); + } + catch (UnknownHostException ignored) { + return false; + } + + if (inetAddress == null) { + return false; + } + return inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress(); + } + @Override protected void browseUsingNotSystemDefaultBrowserPolicy(@NotNull URI uri, @NotNull GeneralSettings settings, @Nullable Project project) { WebBrowserManager browserManager = WebBrowserManager.getInstance(); diff --git a/xml/impl/xml.iml b/xml/impl/xml.iml index 2fb3ee7ea262..fb6c556fefcf 100644 --- a/xml/impl/xml.iml +++ b/xml/impl/xml.iml @@ -23,11 +23,11 @@ + - - + \ No newline at end of file From 275c227e83ea14b73159780b368ed0e991a52996 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Wed, 27 Apr 2016 12:53:35 +0200 Subject: [PATCH 52/67] pass token in the headers in debug mode --- .../BuiltInWebBrowserUrlProvider.java | 9 +++++-- .../builtInWebServer/BuiltInWebServer.kt | 7 ++++-- .../io/DelegatingHttpRequestHandlerBase.kt | 2 +- .../browsers/impl/WebBrowserServiceImpl.java | 11 ++++++-- .../ide/browsers/OpenInBrowserRequest.java | 25 +++++++++++++++++++ 5 files changed, 47 insertions(+), 7 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java index 61e5db5ca9c3..471da19667f8 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java @@ -40,6 +40,11 @@ import java.util.List; public class BuiltInWebBrowserUrlProvider extends WebBrowserUrlProvider implements DumbAware { @NotNull public static List getUrls(@NotNull VirtualFile file, @NotNull Project project, @Nullable String currentAuthority) { + return getUrls(file, project, currentAuthority, true); + } + + @NotNull + public static List getUrls(@NotNull VirtualFile file, @NotNull Project project, @Nullable String currentAuthority, boolean appendAccessToken) { if (currentAuthority != null && !compareAuthority(currentAuthority)) { return Collections.emptyList(); } @@ -53,7 +58,7 @@ public class BuiltInWebBrowserUrlProvider extends WebBrowserUrlProvider implemen String path = info.getPath(); String authority = currentAuthority == null ? "localhost:" + effectiveBuiltInServerPort : currentAuthority; - String query = "?" + BuiltInWebServerKt.TOKEN_PARAM_NAME + "=" + BuiltInWebServerKt.acquireToken(); + String query = appendAccessToken ? "?" + BuiltInWebServerKt.TOKEN_PARAM_NAME + "=" + BuiltInWebServerKt.acquireToken() : ""; List urls = new SmartList<>(Urls.newHttpUrl(authority, '/' + project.getName() + '/' + path, query)); String path2 = info.getRootLessPathIfPossible(); @@ -118,7 +123,7 @@ public class BuiltInWebBrowserUrlProvider extends WebBrowserUrlProvider implemen return Urls.newFromVirtualFile(file); } else { - return ContainerUtil.getFirstItem(getUrls(file, request.getProject(), null)); + return ContainerUtil.getFirstItem(getUrls(file, request.getProject(), null, request.isAppendAccessToken())); } } } diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index 0f3d9b2a31a3..308937b079c9 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -108,6 +108,7 @@ class BuiltInWebServer : HttpRequestHandler() { internal fun isActivatable() = Registry.`is`("ide.built.in.web.server.activatable", false) internal const val TOKEN_PARAM_NAME = "_ijt" +const val TOKEN_HEADER_NAME = "x-ijt" private val STANDARD_COOKIE by lazy { val productName = ApplicationNamesInfo.getInstance().lowercaseProductName @@ -139,7 +140,7 @@ private val STANDARD_COOKIE by lazy { // expire after access because we reuse tokens private val tokens = CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.MINUTES).build() -internal fun acquireToken(): String { +fun acquireToken(): String { var token = tokens.asMap().keys.firstOrNull() if (token == null) { token = TokenGenerator.generate() @@ -252,7 +253,9 @@ internal fun validateToken(request: HttpRequest, channel: Channel): HttpHeaders? val urlDecoder = QueryStringDecoder(request.uri()) // we must check referrer - if html cached, browser will send request without query - val token = urlDecoder.parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() ?: request.referrer?.let { QueryStringDecoder(it).parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() } + val token = request.headers().get(TOKEN_HEADER_NAME) + ?: urlDecoder.parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() + ?: request.referrer?.let { QueryStringDecoder(it).parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() } val url = "${channel.uriScheme}://${request.host!!}${urlDecoder.path()}" if (token != null && tokens.getIfPresent(token) != null) { tokens.invalidate(token) diff --git a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandlerBase.kt b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandlerBase.kt index d8c1914c8e39..b760cddf1561 100644 --- a/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandlerBase.kt +++ b/platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandlerBase.kt @@ -24,7 +24,7 @@ import io.netty.handler.codec.http.QueryStringDecoder internal abstract class DelegatingHttpRequestHandlerBase : SimpleChannelInboundHandlerAdapter() { override fun messageReceived(context: ChannelHandlerContext, message: FullHttpRequest) { - Logger.getInstance(BuiltInServer::class.java).debug { "IN HTTP: $message" } + Logger.getInstance(BuiltInServer::class.java).debug { "\n\nIN HTTP: $message\n\n" } if (!process(context, message, QueryStringDecoder(message.uri()))) { HttpResponseStatus.NOT_FOUND.send(context.channel(), message) diff --git a/xml/impl/src/com/intellij/ide/browsers/impl/WebBrowserServiceImpl.java b/xml/impl/src/com/intellij/ide/browsers/impl/WebBrowserServiceImpl.java index be5151bb06a5..9f23e779ed19 100644 --- a/xml/impl/src/com/intellij/ide/browsers/impl/WebBrowserServiceImpl.java +++ b/xml/impl/src/com/intellij/ide/browsers/impl/WebBrowserServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2015 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -94,7 +94,14 @@ public class WebBrowserServiceImpl extends WebBrowserService { public static Collection getDebuggableUrls(@Nullable PsiElement context) { try { OpenInBrowserRequest request = context == null ? null : OpenInBrowserRequest.create(context); - return request == null || request.getFile().getViewProvider().getBaseLanguage() == XMLLanguage.INSTANCE ? Collections.emptyList() : getUrls(getProvider(request), request); + if (request == null || request.getFile().getViewProvider().getBaseLanguage() == XMLLanguage.INSTANCE) { + return Collections.emptyList(); + } + else { + // it is client responsibility to set token + request.setAppendAccessToken(false); + return getUrls(getProvider(request), request); + } } catch (WebBrowserUrlProvider.BrowserException ignored) { return Collections.emptyList(); diff --git a/xml/openapi/src/com/intellij/ide/browsers/OpenInBrowserRequest.java b/xml/openapi/src/com/intellij/ide/browsers/OpenInBrowserRequest.java index c4119d10c8c0..3f8d70e8522d 100644 --- a/xml/openapi/src/com/intellij/ide/browsers/OpenInBrowserRequest.java +++ b/xml/openapi/src/com/intellij/ide/browsers/OpenInBrowserRequest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.intellij.ide.browsers; import com.intellij.openapi.application.AccessToken; @@ -15,6 +30,8 @@ import java.util.Collection; public abstract class OpenInBrowserRequest { private Collection result; protected PsiFile file; + + private boolean appendAccessToken = true; public OpenInBrowserRequest(@NotNull PsiFile file) { this.file = file; @@ -72,4 +89,12 @@ public abstract class OpenInBrowserRequest { public Collection getResult() { return result; } + + public boolean isAppendAccessToken() { + return appendAccessToken; + } + + public void setAppendAccessToken(boolean value) { + this.appendAccessToken = value; + } } \ No newline at end of file From 63d6cdeba7b5ea6bb77c68fb443557a45baa314f Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Wed, 27 Apr 2016 15:54:01 +0200 Subject: [PATCH 53/67] DefaultRemoteContentProvider must explicitly set token in the headers (Open URL and load remote script by http) --- .../jetbrains/ide/BuiltInServerManager.java | 8 +++ .../builtInWebServer/BuiltInWebServer.kt | 20 ++++--- .../ide/BuiltInServerManagerImpl.java | 55 +++++++++++++++++++ .../http/DefaultRemoteContentProvider.java | 7 +++ .../ide/browsers/BrowserLauncherImpl.java | 50 +---------------- xml/impl/xml.iml | 2 +- 6 files changed, 85 insertions(+), 57 deletions(-) diff --git a/platform/built-in-server-api/src/org/jetbrains/ide/BuiltInServerManager.java b/platform/built-in-server-api/src/org/jetbrains/ide/BuiltInServerManager.java index fff0a1564531..448ede483363 100644 --- a/platform/built-in-server-api/src/org/jetbrains/ide/BuiltInServerManager.java +++ b/platform/built-in-server-api/src/org/jetbrains/ide/BuiltInServerManager.java @@ -18,8 +18,12 @@ package org.jetbrains.ide; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.ApplicationComponent; +import com.intellij.util.Url; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.net.URLConnection; + public abstract class BuiltInServerManager extends ApplicationComponent.Adapter { public static BuiltInServerManager getInstance() { return ApplicationManager.getApplication().getComponent(BuiltInServerManager.class); @@ -31,4 +35,8 @@ public abstract class BuiltInServerManager extends ApplicationComponent.Adapter @Nullable public abstract Disposable getServerDisposable(); + + public abstract boolean isOnBuiltInWebServer(@Nullable Url url); + + public abstract void configureRequestToWebServer(@NotNull URLConnection connection); } \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index 308937b079c9..334cf2ede98f 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -262,16 +262,18 @@ internal fun validateToken(request: HttpRequest, channel: Channel): HttpHeaders? return DefaultHttpHeaders().set(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(STANDARD_COOKIE) + "; SameSite=strict") } - SwingUtilities.invokeAndWait { - ProjectUtil.focusProjectWindow(null, true) + if (!urlDecoder.path().endsWith("/favicon.ico")) { + SwingUtilities.invokeAndWait { + ProjectUtil.focusProjectWindow(null, true) - if (MessageDialogBuilder - .yesNo("", "Page '" + StringUtil.trimMiddle(url, 50) + "' requested without authorization, " + - "\nyou can copy URL and open it in browser to trust it.") - .icon(Messages.getWarningIcon()) - .yesText("Copy authorization URL to clipboard") - .show() == Messages.YES) { - CopyPasteManager.getInstance().setContents(StringSelection(url + "?" + TOKEN_PARAM_NAME + "=" + acquireToken())) + if (MessageDialogBuilder + .yesNo("", "Page '" + StringUtil.trimMiddle(url, 50) + "' requested without authorization, " + + "\nyou can copy URL and open it in browser to trust it.") + .icon(Messages.getWarningIcon()) + .yesText("Copy authorization URL to clipboard") + .show() == Messages.YES) { + CopyPasteManager.getInstance().setContents(StringSelection(url + "?" + TOKEN_PARAM_NAME + "=" + acquireToken())) + } } } diff --git a/platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java b/platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java index 6e33b54cbe24..941a0cb6566b 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java +++ b/platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java @@ -10,13 +10,21 @@ import com.intellij.openapi.application.ApplicationNamesInfo; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.NotNullLazyValue; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.Url; +import com.intellij.util.net.NetUtils; import io.netty.channel.oio.OioEventLoopGroup; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.builtInWebServer.BuiltInServerOptions; +import org.jetbrains.builtInWebServer.BuiltInWebServerKt; import org.jetbrains.io.BuiltInServer; import org.jetbrains.io.SubServer; +import java.net.InetAddress; +import java.net.URLConnection; +import java.net.UnknownHostException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; @@ -116,6 +124,16 @@ public class BuiltInServerManagerImpl extends BuiltInServerManager { return server; } + @Override + public boolean isOnBuiltInWebServer(@Nullable Url url) { + return url != null && !StringUtil.isEmpty(url.getAuthority()) && isOnBuiltInWebServerByAuthority(url.getAuthority()); + } + + @Override + public void configureRequestToWebServer(@NotNull URLConnection connection) { + connection.setRequestProperty(BuiltInWebServerKt.TOKEN_HEADER_NAME, BuiltInWebServerKt.acquireToken()); + } + private static void bindCustomPorts(@NotNull BuiltInServer server) { if (ApplicationManager.getApplication().isUnitTestMode()) { return; @@ -130,4 +148,41 @@ public class BuiltInServerManagerImpl extends BuiltInServerManager { } } } + + public static boolean isOnBuiltInWebServerByAuthority(@NotNull String authority) { + int portIndex = authority.indexOf(':'); + if (portIndex < 0 || portIndex == authority.length() - 1) { + return false; + } + + int port; + try { + port = Integer.parseInt(authority.substring(portIndex + 1)); + } + catch (NumberFormatException ignored) { + return false; + } + + if (BuiltInServerOptions.getInstance().builtInServerPort != port && BuiltInServerManager.getInstance().getPort() != port) { + return false; + } + + String host = authority.substring(0, portIndex); + if (NetUtils.isLocalhost(host)) { + return true; + } + + InetAddress inetAddress; + try { + inetAddress = InetAddress.getByName(host); + } + catch (UnknownHostException ignored) { + return false; + } + + if (inetAddress == null) { + return false; + } + return inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress(); + } } \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/impl/http/DefaultRemoteContentProvider.java b/platform/platform-impl/src/com/intellij/openapi/vfs/impl/http/DefaultRemoteContentProvider.java index c74cecb41df7..4f0baf1bd92f 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/impl/http/DefaultRemoteContentProvider.java +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/impl/http/DefaultRemoteContentProvider.java @@ -29,6 +29,7 @@ import com.intellij.util.Url; import com.intellij.util.io.HttpRequests; import com.intellij.util.net.ssl.CertificateManager; import org.jetbrains.annotations.NotNull; +import org.jetbrains.ide.BuiltInServerManager; import java.io.File; import java.io.IOException; @@ -60,6 +61,12 @@ public class DefaultRemoteContentProvider extends RemoteContentProvider { .connectTimeout(60 * 1000) .productNameAsUserAgent() .hostNameVerifier(CertificateManager.HOSTNAME_VERIFIER) + .tuner(connection -> { + BuiltInServerManager builtInServerManager = BuiltInServerManager.getInstance(); + if (builtInServerManager.isOnBuiltInWebServer(url)) { + builtInServerManager.configureRequestToWebServer(connection); + } + }) .connect(new HttpRequests.RequestProcessor() { @Override public Object process(@NotNull HttpRequests.Request request) throws IOException { diff --git a/xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java b/xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java index 7a62abe0b33f..4b509f7119a3 100644 --- a/xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java +++ b/xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java @@ -30,70 +30,26 @@ import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.ui.AppUIUtil; import com.intellij.util.ArrayUtil; -import com.intellij.util.Url; import com.intellij.util.Urls; -import com.intellij.util.net.NetUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.builtInWebServer.BuiltInServerOptions; import org.jetbrains.ide.BuiltInServerManager; -import java.net.InetAddress; import java.net.URI; -import java.net.UnknownHostException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; public final class BrowserLauncherImpl extends BrowserLauncherAppless { @Override public void browse(@NotNull String url, @Nullable WebBrowser browser, @Nullable Project project) { - if (Registry.is("ide.built.in.web.server.activatable", false)) { - Url parsedUrl = Urls.parse(url, false); - if (parsedUrl != null && parsedUrl.getAuthority() != null && isOnBuiltInWebServerByAuthority(parsedUrl.getAuthority())) { - PropertiesComponent.getInstance().setValue("ide.built.in.web.server.active", true); - } + if (Registry.is("ide.built.in.web.server.activatable", false) && + BuiltInServerManager.getInstance().isOnBuiltInWebServer(Urls.parse(url, false))) { + PropertiesComponent.getInstance().setValue("ide.built.in.web.server.active", true); } super.browse(url, browser, project); } - public static boolean isOnBuiltInWebServerByAuthority(@NotNull String authority) { - int portIndex = authority.indexOf(':'); - if (portIndex < 0 || portIndex == authority.length() - 1) { - return false; - } - - int port; - try { - port = Integer.parseInt(authority.substring(portIndex + 1)); - } - catch (NumberFormatException ignored) { - return false; - } - - if (BuiltInServerOptions.getInstance().builtInServerPort != port && BuiltInServerManager.getInstance().getPort() != port) { - return false; - } - - String host = authority.substring(0, portIndex); - if (NetUtils.isLocalhost(host)) { - return true; - } - - InetAddress inetAddress; - try { - inetAddress = InetAddress.getByName(host); - } - catch (UnknownHostException ignored) { - return false; - } - - if (inetAddress == null) { - return false; - } - return inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress(); - } - @Override protected void browseUsingNotSystemDefaultBrowserPolicy(@NotNull URI uri, @NotNull GeneralSettings settings, @Nullable Project project) { WebBrowserManager browserManager = WebBrowserManager.getInstance(); diff --git a/xml/impl/xml.iml b/xml/impl/xml.iml index fb6c556fefcf..447d4923d47e 100644 --- a/xml/impl/xml.iml +++ b/xml/impl/xml.iml @@ -23,7 +23,7 @@ - + From 7524a66754c623042348ede0efc14205a8df1d07 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Wed, 27 Apr 2016 15:59:54 +0200 Subject: [PATCH 54/67] set correct file permissions --- .../builtInWebServer/BuiltInWebServer.kt | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index 334cf2ede98f..dae131732a64 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -36,9 +36,8 @@ import com.intellij.openapi.util.io.endsWithName import com.intellij.openapi.util.registry.Registry import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.VirtualFile -import com.intellij.util.directoryStreamIfExists +import com.intellij.util.* import com.intellij.util.io.URLUtil -import com.intellij.util.isDirectory import com.intellij.util.net.NetUtils import io.netty.channel.Channel import io.netty.channel.ChannelHandlerContext @@ -51,11 +50,14 @@ import org.jetbrains.ide.HttpRequestHandler import org.jetbrains.io.* import org.jetbrains.notification.SingletonNotificationManager import java.awt.datatransfer.StringSelection -import java.io.File import java.io.IOException import java.math.BigInteger import java.net.InetAddress +import java.nio.file.Files import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.attribute.PosixFileAttributeView +import java.nio.file.attribute.PosixFilePermission import java.security.SecureRandom import java.util.* import java.util.concurrent.TimeUnit @@ -113,11 +115,11 @@ const val TOKEN_HEADER_NAME = "x-ijt" private val STANDARD_COOKIE by lazy { val productName = ApplicationNamesInfo.getInstance().lowercaseProductName val configPath = PathManager.getConfigPath() - val file = File(configPath, IDE_TOKEN_FILE) + val file = Paths.get(configPath, IDE_TOKEN_FILE) var token: String? = null if (file.exists()) { try { - token = UUID.fromString(FileUtil.loadFile(file)).toString() + token = UUID.fromString(file.readText()).toString() } catch (e: Exception) { LOG.warn(e) @@ -125,7 +127,16 @@ private val STANDARD_COOKIE by lazy { } if (token == null) { token = UUID.randomUUID().toString() - FileUtil.writeToFile(file, token!!) + file.write(token!!) + val view = Files.getFileAttributeView(file, PosixFileAttributeView::class.java) + if (view != null) { + try { + view.setPermissions(setOf(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE)) + } + catch (e: IOException) { + LOG.warn(e) + } + } } // explicit setting domain cookie on localhost doesn't work for chrome From dfc16163f79997993e3d3336b8825b3228217e51 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Thu, 28 Apr 2016 13:23:26 +0200 Subject: [PATCH 55/67] fix isOnBuiltInWebServerByAuthority for external IP --- .../ide/BuiltInServerManagerImpl.java | 27 ++++++------------- .../http/DefaultRemoteContentProvider.java | 18 ++++++++----- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java b/platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java index 941a0cb6566b..15d4a4cdf137 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java +++ b/platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java @@ -22,9 +22,10 @@ import org.jetbrains.builtInWebServer.BuiltInWebServerKt; import org.jetbrains.io.BuiltInServer; import org.jetbrains.io.SubServer; +import java.io.IOException; import java.net.InetAddress; +import java.net.NetworkInterface; import java.net.URLConnection; -import java.net.UnknownHostException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; @@ -148,22 +149,15 @@ public class BuiltInServerManagerImpl extends BuiltInServerManager { } } } - + public static boolean isOnBuiltInWebServerByAuthority(@NotNull String authority) { int portIndex = authority.indexOf(':'); if (portIndex < 0 || portIndex == authority.length() - 1) { return false; } - int port; - try { - port = Integer.parseInt(authority.substring(portIndex + 1)); - } - catch (NumberFormatException ignored) { - return false; - } - - if (BuiltInServerOptions.getInstance().builtInServerPort != port && BuiltInServerManager.getInstance().getPort() != port) { + int port = StringUtil.parseInt(authority.substring(portIndex + 1), -1); + if (port == -1 || (BuiltInServerOptions.getInstance().builtInServerPort != port && BuiltInServerManager.getInstance().getPort() != port)) { return false; } @@ -172,17 +166,12 @@ public class BuiltInServerManagerImpl extends BuiltInServerManager { return true; } - InetAddress inetAddress; try { - inetAddress = InetAddress.getByName(host); + InetAddress inetAddress = InetAddress.getByName(host); + return inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() || NetworkInterface.getByInetAddress(inetAddress) != null; } - catch (UnknownHostException ignored) { + catch (IOException e) { return false; } - - if (inetAddress == null) { - return false; - } - return inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress(); } } \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/impl/http/DefaultRemoteContentProvider.java b/platform/platform-impl/src/com/intellij/openapi/vfs/impl/http/DefaultRemoteContentProvider.java index 4f0baf1bd92f..c3b81522c35b 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/impl/http/DefaultRemoteContentProvider.java +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/impl/http/DefaultRemoteContentProvider.java @@ -27,6 +27,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.PathUtilRt; import com.intellij.util.Url; import com.intellij.util.io.HttpRequests; +import com.intellij.util.io.RequestBuilder; import com.intellij.util.net.ssl.CertificateManager; import org.jetbrains.annotations.NotNull; import org.jetbrains.ide.BuiltInServerManager; @@ -37,6 +38,15 @@ import java.io.IOException; public class DefaultRemoteContentProvider extends RemoteContentProvider { private static final Logger LOG = Logger.getInstance(DefaultRemoteContentProvider.class); + @NotNull + public static RequestBuilder addRequestTuner(@NotNull Url url, @NotNull RequestBuilder requestBuilder) { + BuiltInServerManager builtInServerManager = BuiltInServerManager.getInstance(); + if (builtInServerManager.isOnBuiltInWebServer(url)) { + requestBuilder.tuner(builtInServerManager::configureRequestToWebServer); + } + return requestBuilder; + } + @Override public boolean canProvideContent(@NotNull Url url) { return true; @@ -57,16 +67,10 @@ public class DefaultRemoteContentProvider extends RemoteContentProvider { final String presentableUrl = StringUtil.trimMiddle(url.trimParameters().toDecodedForm(), 40); callback.setProgressText(VfsBundle.message("download.progress.connecting", presentableUrl), true); try { - HttpRequests.request(url.toExternalForm()) + addRequestTuner(url, HttpRequests.request(url.toExternalForm())) .connectTimeout(60 * 1000) .productNameAsUserAgent() .hostNameVerifier(CertificateManager.HOSTNAME_VERIFIER) - .tuner(connection -> { - BuiltInServerManager builtInServerManager = BuiltInServerManager.getInstance(); - if (builtInServerManager.isOnBuiltInWebServer(url)) { - builtInServerManager.configureRequestToWebServer(connection); - } - }) .connect(new HttpRequests.RequestProcessor() { @Override public Object process(@NotNull HttpRequests.Request request) throws IOException { From 7437b6993f3f55f0f2379ec1e0619aa8b8f60983 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Thu, 28 Apr 2016 14:44:16 +0200 Subject: [PATCH 56/67] check NetworkInterface only if custom port and available externally --- .../org/jetbrains/ide/BuiltInServerManagerImpl.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java b/platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java index 15d4a4cdf137..54cd4b34decb 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java +++ b/platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java @@ -157,7 +157,13 @@ public class BuiltInServerManagerImpl extends BuiltInServerManager { } int port = StringUtil.parseInt(authority.substring(portIndex + 1), -1); - if (port == -1 || (BuiltInServerOptions.getInstance().builtInServerPort != port && BuiltInServerManager.getInstance().getPort() != port)) { + if (port == -1) { + return false; + } + + BuiltInServerOptions options = BuiltInServerOptions.getInstance(); + int idePort = BuiltInServerManager.getInstance().getPort(); + if (options.builtInServerPort != port && idePort != port) { return false; } @@ -168,7 +174,9 @@ public class BuiltInServerManagerImpl extends BuiltInServerManager { try { InetAddress inetAddress = InetAddress.getByName(host); - return inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() || NetworkInterface.getByInetAddress(inetAddress) != null; + return inetAddress.isLoopbackAddress() || + inetAddress.isAnyLocalAddress() || + (options.builtInServerAvailableExternally && idePort != port && NetworkInterface.getByInetAddress(inetAddress) != null); } catch (IOException e) { return false; From 2e7e0608d73d358d6a72dbe1ddbfdf106a001f49 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Thu, 28 Apr 2016 18:09:07 +0200 Subject: [PATCH 57/67] IDEA-155360 Open In Browser : files from library jars cannot be opened --- .../builtInWebServer/BuiltInWebServer.kt | 35 ++++++++++++------- .../DefaultWebServerPathHandler.kt | 5 +-- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index dae131732a64..513a36b4a38f 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -248,11 +248,25 @@ private fun doProcess(urlDecoder: QueryStringDecoder, request: FullHttpRequest, return false } -internal fun validateToken(request: HttpRequest, channel: Channel): HttpHeaders? { - val cookieString = request.headers().get(HttpHeaderNames.COOKIE) - if (cookieString != null) { - val cookies = ServerCookieDecoder.STRICT.decode(cookieString) - for (cookie in cookies) { +internal fun HttpRequest.isSignedRequest(): Boolean { + // we must check referrer - if html cached, browser will send request without query + val token = headers().get(TOKEN_HEADER_NAME) + ?: QueryStringDecoder(uri()).parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() + ?: referrer?.let { QueryStringDecoder(it).parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() } + + if (token != null && tokens.getIfPresent(token) != null) { + tokens.invalidate(token) + return true + } + else { + return false + } +} + +@JvmOverloads +internal fun validateToken(request: HttpRequest, channel: Channel, isSignedRequest: Boolean = request.isSignedRequest()): HttpHeaders? { + request.headers().get(HttpHeaderNames.COOKIE)?.let { + for (cookie in ServerCookieDecoder.STRICT.decode(it)) { if (cookie.name() == STANDARD_COOKIE.name()) { if (cookie.value() == STANDARD_COOKIE.value()) { return EmptyHttpHeaders.INSTANCE @@ -262,18 +276,13 @@ internal fun validateToken(request: HttpRequest, channel: Channel): HttpHeaders? } } - val urlDecoder = QueryStringDecoder(request.uri()) - // we must check referrer - if html cached, browser will send request without query - val token = request.headers().get(TOKEN_HEADER_NAME) - ?: urlDecoder.parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() - ?: request.referrer?.let { QueryStringDecoder(it).parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() } - val url = "${channel.uriScheme}://${request.host!!}${urlDecoder.path()}" - if (token != null && tokens.getIfPresent(token) != null) { - tokens.invalidate(token) + if (isSignedRequest) { return DefaultHttpHeaders().set(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(STANDARD_COOKIE) + "; SameSite=strict") } + val urlDecoder = QueryStringDecoder(request.uri()) if (!urlDecoder.path().endsWith("/favicon.ico")) { + val url = "${channel.uriScheme}://${request.host!!}${urlDecoder.path()}" SwingUtilities.invokeAndWait { ProjectUtil.focusProjectWindow(null, true) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt index e88bb2c98193..7f8c575a1a9b 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt @@ -41,7 +41,8 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { projectName: String, decodedRawPath: String, isCustomHost: Boolean): Boolean { - val extraHttpHeaders = validateToken(request, context.channel()) ?: return true + val isSignedRequest = request.isSignedRequest() + val extraHttpHeaders = validateToken(request, context.channel(), isSignedRequest) ?: return true val channel = context.channel() val pathToFileManager = WebServerPathToFileManager.getInstance(project) @@ -89,7 +90,7 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { } // if extraHttpHeaders is not empty, it means that we get request wih token in the query - if (extraHttpHeaders.isEmpty && request.origin == null && request.referrer == null && request.isRegularBrowser() && !canBeAccessedDirectly(pathInfo.name)) { + if (!isSignedRequest && request.origin == null && request.referrer == null && request.isRegularBrowser() && !canBeAccessedDirectly(pathInfo.name)) { HttpResponseStatus.NOT_FOUND.send(context.channel(), request) return true } From 077e7461222bfa58f5388330d44e5d4a6fcd6f8e Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Thu, 28 Apr 2016 19:22:14 +0200 Subject: [PATCH 58/67] WEB-21340 Support underscore symbol in the custom domain name defined via hosts WEB-21447 Attached files (.css, .js) are not served by built-in webserver --- .../src/org/jetbrains/io/netty.kt | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/platform/platform-impl/src/org/jetbrains/io/netty.kt b/platform/platform-impl/src/org/jetbrains/io/netty.kt index 0d0c87f8d090..044302fdb578 100644 --- a/platform/platform-impl/src/org/jetbrains/io/netty.kt +++ b/platform/platform-impl/src/org/jetbrains/io/netty.kt @@ -18,6 +18,8 @@ package org.jetbrains.io import com.google.common.net.InetAddresses import com.intellij.openapi.util.Condition import com.intellij.openapi.util.Conditions +import com.intellij.util.Url +import com.intellij.util.Urls import com.intellij.util.net.NetUtils import io.netty.bootstrap.Bootstrap import io.netty.bootstrap.ServerBootstrap @@ -40,7 +42,6 @@ import java.io.IOException import java.net.InetAddress import java.net.InetSocketAddress import java.net.NetworkInterface -import java.net.URI import java.util.concurrent.TimeUnit inline fun Bootstrap.handler(crossinline task: (Channel) -> Unit): Bootstrap { @@ -67,8 +68,7 @@ fun oioClientBootstrap(): Bootstrap { } inline fun ChannelFuture.addChannelListener(crossinline listener: (future: ChannelFuture) -> Unit) { - @Suppress("RedundantSamConstructor") - addListener(GenericFutureListener { listener(it) }) + addListener(GenericFutureListener { listener(it) }) } // if NIO, so, it is shared and we must not shutdown it @@ -164,10 +164,16 @@ fun isLocalHost(host: String, onlyAnyOrLoopback: Boolean, hostsOnly: Boolean = f @JvmOverloads fun HttpRequest.isLocalOrigin(onlyAnyOrLoopback: Boolean = true, hostsOnly: Boolean = false) = parseAndCheckIsLocalHost(origin, onlyAnyOrLoopback, hostsOnly) && parseAndCheckIsLocalHost(referrer, onlyAnyOrLoopback, hostsOnly) -private fun isTrustedChromeExtension(uri: URI): Boolean { - return uri.scheme == "chrome-extension" && (uri.host == "hmhgeddbohgjknpmjagkdomcpobmllji" || uri.host == "offnedcbhjldheanlbojaefbfbllddna") +private fun isTrustedChromeExtension(url: Url): Boolean { + return url.scheme == "chrome-extension" && (url.authority == "hmhgeddbohgjknpmjagkdomcpobmllji" || url.authority == "offnedcbhjldheanlbojaefbfbllddna") } +private val Url.host: String? + get() = authority?.let { + val portIndex = it.indexOf(':') + if (portIndex > 0) it.substring(0, portIndex) else it + } + @JvmOverloads fun parseAndCheckIsLocalHost(uri: String?, onlyAnyOrLoopback: Boolean = true, hostsOnly: Boolean = false): Boolean { if (uri == null) { @@ -175,8 +181,9 @@ fun parseAndCheckIsLocalHost(uri: String?, onlyAnyOrLoopback: Boolean = true, ho } try { - val parsedUri = URI(uri) - return isTrustedChromeExtension(parsedUri) || isLocalHost(parsedUri.host, onlyAnyOrLoopback, hostsOnly) + val parsedUri = Urls.parse(uri, false) ?: return false + val host = parsedUri.host + return host != null && (isTrustedChromeExtension(parsedUri) || isLocalHost(host, onlyAnyOrLoopback, hostsOnly)) } catch (ignored: Exception) { } From a3fa84929166b00965bc5a3b9c7059b9d59a83c6 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 29 Apr 2016 11:30:59 +0200 Subject: [PATCH 59/67] if we in the DefaultWebServerPathHandler, it means that we should or handle request, or return 404 --- .../DefaultWebServerPathHandler.kt | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt index 7f8c575a1a9b..2b2a53cdfb0a 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt @@ -41,22 +41,18 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { projectName: String, decodedRawPath: String, isCustomHost: Boolean): Boolean { - val isSignedRequest = request.isSignedRequest() - val extraHttpHeaders = validateToken(request, context.channel(), isSignedRequest) ?: return true - val channel = context.channel() + + val isSignedRequest = request.isSignedRequest() + val extraHttpHeaders = validateToken(request, channel, isSignedRequest) ?: return true + val pathToFileManager = WebServerPathToFileManager.getInstance(project) var pathInfo = pathToFileManager.pathToInfoCache.getIfPresent(path) if (pathInfo == null || !pathInfo.isValid) { pathInfo = pathToFileManager.doFindByRelativePath(path) if (pathInfo == null) { - if (path.isEmpty()) { - HttpResponseStatus.NOT_FOUND.send(channel, request, "Index file doesn't exist.", extraHttpHeaders) - return true - } - else { - return false - } + HttpResponseStatus.NOT_FOUND.send(channel, request, if (path.isEmpty()) "Index file doesn't exist." else null, extraHttpHeaders) + return true } pathToFileManager.pathToInfoCache.put(path, pathInfo) @@ -91,7 +87,7 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { // if extraHttpHeaders is not empty, it means that we get request wih token in the query if (!isSignedRequest && request.origin == null && request.referrer == null && request.isRegularBrowser() && !canBeAccessedDirectly(pathInfo.name)) { - HttpResponseStatus.NOT_FOUND.send(context.channel(), request) + HttpResponseStatus.NOT_FOUND.send(channel, request) return true } @@ -121,7 +117,10 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { } } } - return false + + // we registered as a last handler, so, we should just return 404 and send extra headers + HttpResponseStatus.NOT_FOUND.send(channel, request, extraHeaders = extraHttpHeaders) + return true } } From fbeb8ac7cd7e7a8417f26004ff7ffa7eff14d97b Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 29 Apr 2016 13:51:34 +0200 Subject: [PATCH 60/67] run RC shoud add token always --- .../org/jetbrains/ide/BuiltInServerManager.java | 6 ++++-- .../jetbrains/ide/BuiltInServerManagerImpl.java | 10 ++++++++++ .../intellij/ide/browsers/BrowserLauncherImpl.java | 14 ++++++++++---- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/platform/built-in-server-api/src/org/jetbrains/ide/BuiltInServerManager.java b/platform/built-in-server-api/src/org/jetbrains/ide/BuiltInServerManager.java index 448ede483363..bd222fa5a57f 100644 --- a/platform/built-in-server-api/src/org/jetbrains/ide/BuiltInServerManager.java +++ b/platform/built-in-server-api/src/org/jetbrains/ide/BuiltInServerManager.java @@ -35,8 +35,10 @@ public abstract class BuiltInServerManager extends ApplicationComponent.Adapter @Nullable public abstract Disposable getServerDisposable(); - + public abstract boolean isOnBuiltInWebServer(@Nullable Url url); - + public abstract void configureRequestToWebServer(@NotNull URLConnection connection); + + public abstract Url addAuthToken(@NotNull Url url); } \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java b/platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java index 54cd4b34decb..3580882d9bf1 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java +++ b/platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java @@ -12,6 +12,7 @@ import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.NotNullLazyValue; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.Url; +import com.intellij.util.UrlImpl; import com.intellij.util.net.NetUtils; import io.netty.channel.oio.OioEventLoopGroup; import org.jetbrains.annotations.NonNls; @@ -130,6 +131,15 @@ public class BuiltInServerManagerImpl extends BuiltInServerManager { return url != null && !StringUtil.isEmpty(url.getAuthority()) && isOnBuiltInWebServerByAuthority(url.getAuthority()); } + @Override + public Url addAuthToken(@NotNull Url url) { + if (url.getParameters() != null) { + // built-in server url contains query only if token specified + return url; + } + return new UrlImpl(url.getScheme(), url.getAuthority(), url.getPath(), "?" + BuiltInWebServerKt.TOKEN_PARAM_NAME + "=" + BuiltInWebServerKt.acquireToken()); + } + @Override public void configureRequestToWebServer(@NotNull URLConnection connection) { connection.setRequestProperty(BuiltInWebServerKt.TOKEN_HEADER_NAME, BuiltInWebServerKt.acquireToken()); diff --git a/xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java b/xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java index 4b509f7119a3..ab47ac9d98f4 100644 --- a/xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java +++ b/xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java @@ -30,6 +30,7 @@ import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.ui.AppUIUtil; import com.intellij.util.ArrayUtil; +import com.intellij.util.Url; import com.intellij.util.Urls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -42,11 +43,16 @@ import java.util.concurrent.TimeUnit; public final class BrowserLauncherImpl extends BrowserLauncherAppless { @Override public void browse(@NotNull String url, @Nullable WebBrowser browser, @Nullable Project project) { - if (Registry.is("ide.built.in.web.server.activatable", false) && - BuiltInServerManager.getInstance().isOnBuiltInWebServer(Urls.parse(url, false))) { - PropertiesComponent.getInstance().setValue("ide.built.in.web.server.active", true); + BuiltInServerManager serverManager = BuiltInServerManager.getInstance(); + Url parsedUrl = Urls.parse(url, false); + if (parsedUrl != null && serverManager.isOnBuiltInWebServer(parsedUrl)) { + if (Registry.is("ide.built.in.web.server.activatable", false)) { + PropertiesComponent.getInstance().setValue("ide.built.in.web.server.active", true); + } + + url = serverManager.addAuthToken(parsedUrl).toExternalForm(); } - + super.browse(url, browser, project); } From 42c40db21b2b74992b4e46336b0b80ae41373e16 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 29 Apr 2016 15:26:12 +0200 Subject: [PATCH 61/67] IDEA-155362 Open In Browser : action should work for files versons from History or be disabled --- .../ide/browsers/actions/BaseOpenInBrowserAction.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/xml/impl/src/com/intellij/ide/browsers/actions/BaseOpenInBrowserAction.java b/xml/impl/src/com/intellij/ide/browsers/actions/BaseOpenInBrowserAction.java index dcf8d0895108..fef95a602470 100644 --- a/xml/impl/src/com/intellij/ide/browsers/actions/BaseOpenInBrowserAction.java +++ b/xml/impl/src/com/intellij/ide/browsers/actions/BaseOpenInBrowserAction.java @@ -30,6 +30,7 @@ import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.openapi.vcs.vfs.ContentRevisionVirtualFile; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; @@ -118,7 +119,7 @@ public abstract class BaseOpenInBrowserAction extends DumbAwareAction { if (psiFile == null) { psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()); } - if (psiFile != null) { + if (psiFile != null && !(psiFile.getVirtualFile() instanceof ContentRevisionVirtualFile)) { return new OpenInBrowserRequest(psiFile) { private PsiElement element; @@ -142,7 +143,7 @@ public abstract class BaseOpenInBrowserAction extends DumbAwareAction { psiFile = PsiManager.getInstance(project).findFile(virtualFile); } - if (psiFile != null) { + if (psiFile != null && !(psiFile.getVirtualFile() instanceof ContentRevisionVirtualFile)) { return OpenInBrowserRequest.create(psiFile); } } From 02154bafe629835d6a70fee1d86c0552f08f9199 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Thu, 12 May 2016 14:24:08 +0200 Subject: [PATCH 62/67] upsource trust to configured host --- .../built-in-server/src/org/jetbrains/ide/RestService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platform/built-in-server/src/org/jetbrains/ide/RestService.java b/platform/built-in-server/src/org/jetbrains/ide/RestService.java index 877ac39cdfff..7283225dc0ae 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/RestService.java +++ b/platform/built-in-server/src/org/jetbrains/ide/RestService.java @@ -175,7 +175,8 @@ public abstract class RestService extends HttpRequestHandler { return true; } - private boolean isHostTrusted(@NotNull FullHttpRequest request) throws InterruptedException, InvocationTargetException { + // e.g. upsource trust to configured host + protected boolean isHostTrusted(@NotNull FullHttpRequest request) throws InterruptedException, InvocationTargetException { String referrer = NettyKt.getOrigin(request); if (referrer == null) { referrer = NettyKt.getReferrer(request); From 27650a3c10bc34068e17178357bddac9af338ce5 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Thu, 12 May 2016 17:15:32 +0200 Subject: [PATCH 63/67] IDEA-155871 DocumentationComponent must append access token to image requests --- .../builtInWebServer/BuiltInWebServer.kt | 9 ++----- platform/lang-impl/lang-impl.iml | 1 + .../documentation/DocumentationComponent.java | 24 +++++++++++++++---- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index 513a36b4a38f..9ba1e8652603 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -254,13 +254,8 @@ internal fun HttpRequest.isSignedRequest(): Boolean { ?: QueryStringDecoder(uri()).parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() ?: referrer?.let { QueryStringDecoder(it).parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() } - if (token != null && tokens.getIfPresent(token) != null) { - tokens.invalidate(token) - return true - } - else { - return false - } + // we don't invalidate token — allow to make subsequent requests using it (it is required for our javadoc DocumentationComponent) + return token != null && tokens.getIfPresent(token) != null } @JvmOverloads diff --git a/platform/lang-impl/lang-impl.iml b/platform/lang-impl/lang-impl.iml index f3ff6f061860..aa2443f54993 100644 --- a/platform/lang-impl/lang-impl.iml +++ b/platform/lang-impl/lang-impl.iml @@ -30,5 +30,6 @@ + \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationComponent.java b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationComponent.java index 645a591f4f45..727aaab6ee74 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationComponent.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2015 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import com.intellij.icons.AllIcons; import com.intellij.ide.DataManager; import com.intellij.ide.actions.BaseNavigateToSourceAction; import com.intellij.ide.actions.ExternalJavaDocAction; -import com.intellij.lang.documentation.CompositeDocumentationProvider; import com.intellij.lang.documentation.DocumentationProvider; import com.intellij.lang.documentation.ExternalDocumentationProvider; import com.intellij.openapi.Disposable; @@ -43,7 +42,6 @@ import com.intellij.openapi.ui.popup.JBPopup; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.InvalidDataException; import com.intellij.openapi.util.registry.Registry; -import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.wm.ex.WindowManagerEx; import com.intellij.pom.Navigatable; import com.intellij.psi.PsiElement; @@ -56,6 +54,8 @@ import com.intellij.ui.SideBorder; import com.intellij.ui.components.JBLayeredPane; import com.intellij.ui.components.JBScrollPane; import com.intellij.util.Consumer; +import com.intellij.util.Url; +import com.intellij.util.Urls; import com.intellij.util.containers.HashMap; import com.intellij.util.ui.GraphicsUtil; import com.intellij.util.ui.JBDimension; @@ -64,6 +64,7 @@ import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.ide.BuiltInServerManager; import javax.swing.*; import javax.swing.event.ChangeEvent; @@ -75,6 +76,7 @@ import javax.swing.text.html.HTML; import javax.swing.text.html.HTMLDocument; import java.awt.*; import java.awt.event.*; +import java.net.MalformedURLException; import java.net.URL; import java.util.*; import java.util.List; @@ -113,7 +115,21 @@ public class DocumentationComponent extends JPanel implements Disposable, DataPr if (element == null) return null; URL url = (URL)key; Image inMemory = myManager.getElementImage(element, url.toExternalForm()); - return inMemory != null ? inMemory : Toolkit.getDefaultToolkit().createImage(url); + if (inMemory != null) { + return inMemory; + } + + Url parsedUrl = Urls.parseEncoded(url.toExternalForm()); + BuiltInServerManager builtInServerManager = BuiltInServerManager.getInstance(); + if (parsedUrl != null && builtInServerManager.isOnBuiltInWebServer(parsedUrl)) { + try { + url = new URL(builtInServerManager.addAuthToken(parsedUrl).toExternalForm()); + } + catch (MalformedURLException e) { + LOGGER.warn(e); + } + } + return Toolkit.getDefaultToolkit().createImage(url); } }; From ae634a24b4a1e2de68c5552db2715dc143437fa1 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Thu, 12 May 2016 18:42:45 +0200 Subject: [PATCH 64/67] add "Allow unsigned requests" --- .../BuiltInServerConfigurableUi.form | 30 ++++++++++++------- .../BuiltInServerConfigurableUi.java | 5 +++- .../BuiltInServerOptions.java | 3 ++ .../builtInWebServer/BuiltInWebServer.kt | 8 +++++ .../src/messages/XmlBundle.properties | 3 +- 5 files changed, 36 insertions(+), 13 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInServerConfigurableUi.form b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInServerConfigurableUi.form index 3aa78b3be81c..fc29de8d45d3 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInServerConfigurableUi.form +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInServerConfigurableUi.form @@ -1,6 +1,6 @@
- + @@ -8,14 +8,6 @@ - - - - - - - - @@ -30,11 +22,27 @@ - + - + + + + + + + + + + + + + + + + + diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInServerConfigurableUi.java b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInServerConfigurableUi.java index edbef65543af..1747e0dbb2f3 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInServerConfigurableUi.java +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInServerConfigurableUi.java @@ -11,6 +11,7 @@ class BuiltInServerConfigurableUi implements ConfigurableUi Date: Fri, 13 May 2016 15:55:57 +0200 Subject: [PATCH 65/67] cleanup --- .../DefaultWebServerPathHandler.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt index 2b2a53cdfb0a..b81fac71a332 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt @@ -44,14 +44,14 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { val channel = context.channel() val isSignedRequest = request.isSignedRequest() - val extraHttpHeaders = validateToken(request, channel, isSignedRequest) ?: return true + val extraHeaders = validateToken(request, channel, isSignedRequest) ?: return true val pathToFileManager = WebServerPathToFileManager.getInstance(project) var pathInfo = pathToFileManager.pathToInfoCache.getIfPresent(path) if (pathInfo == null || !pathInfo.isValid) { pathInfo = pathToFileManager.doFindByRelativePath(path) if (pathInfo == null) { - HttpResponseStatus.NOT_FOUND.send(channel, request, if (path.isEmpty()) "Index file doesn't exist." else null, extraHttpHeaders) + HttpResponseStatus.NOT_FOUND.send(channel, request, extraHeaders = extraHeaders) return true } @@ -70,13 +70,13 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { } if (indexFile == null && indexVirtualFile == null) { - HttpResponseStatus.NOT_FOUND.send(channel, request, extraHeaders = extraHttpHeaders) + HttpResponseStatus.NOT_FOUND.send(channel, request, extraHeaders = extraHeaders) return true } // we must redirect only after index file check to not expose directory status if (!endsWithSlash(decodedRawPath)) { - redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path", extraHttpHeaders) + redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path", extraHeaders) return true } @@ -87,7 +87,7 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { // if extraHttpHeaders is not empty, it means that we get request wih token in the query if (!isSignedRequest && request.origin == null && request.referrer == null && request.isRegularBrowser() && !canBeAccessedDirectly(pathInfo.name)) { - HttpResponseStatus.NOT_FOUND.send(channel, request) + HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request) return true } @@ -99,7 +99,7 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { // FallbackResource feature in action, /login requested, /index.php retrieved, we must not redirect /login to /login/ val parentPath = getParentPath(pathInfo.path) if (parentPath != null && endsWithName(path, PathUtilRt.getFileName(parentPath))) { - redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path", extraHttpHeaders) + redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path", extraHeaders) return true } } @@ -112,14 +112,14 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { val canonicalPath = if (indexUsed) "$path/${pathInfo.name}" else path for (fileHandler in WebServerFileHandler.EP_NAME.extensions) { LOG.catchAndLog { - if (fileHandler.process(pathInfo!!, canonicalPath, project, request, channel, if (isCustomHost) null else projectName, extraHttpHeaders)) { + if (fileHandler.process(pathInfo!!, canonicalPath, project, request, channel, if (isCustomHost) null else projectName, extraHeaders)) { return true } } } // we registered as a last handler, so, we should just return 404 and send extra headers - HttpResponseStatus.NOT_FOUND.send(channel, request, extraHeaders = extraHttpHeaders) + HttpResponseStatus.NOT_FOUND.send(channel, request, extraHeaders = extraHeaders) return true } } From a27dd8d2521684bd7d02446f37108ad13a17ff11 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 13 May 2016 17:18:24 +0200 Subject: [PATCH 66/67] WEB-21612 Chrome not loading ts files after security update --- .../builtInWebServer/BuiltInWebServer.kt | 12 --------- .../DefaultWebServerPathHandler.kt | 27 ++++++++++++++++--- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt index c07b496e5e3a..e8d95620fac7 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt @@ -31,7 +31,6 @@ import com.intellij.openapi.ui.MessageDialogBuilder import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.SystemInfoRt import com.intellij.openapi.util.io.FileUtil -import com.intellij.openapi.util.io.FileUtilRt import com.intellij.openapi.util.io.endsWithName import com.intellij.openapi.util.registry.Registry import com.intellij.openapi.util.text.StringUtil @@ -394,15 +393,4 @@ internal fun isOwnHostName(host: String): Boolean { catch (ignored: IOException) { return false } -} - -internal fun canBeAccessedDirectly(path: String): Boolean { - for (fileHandler in WebServerFileHandler.EP_NAME.extensions) { - for (ext in fileHandler.pageFileExtensions) { - if (FileUtilRt.extensionEquals(path, ext)) { - return true - } - } - } - return false } \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt index b81fac71a332..2876c923e9bd 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt @@ -17,9 +17,11 @@ package org.jetbrains.builtInWebServer import com.intellij.openapi.diagnostic.catchAndLog import com.intellij.openapi.project.Project +import com.intellij.openapi.util.io.FileUtilRt import com.intellij.openapi.util.io.endsWithName import com.intellij.openapi.util.io.endsWithSlash import com.intellij.openapi.util.io.getParentPath +import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.VFileProperty import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.PathUtilRt @@ -32,6 +34,9 @@ import io.netty.handler.codec.http.HttpResponseStatus import org.jetbrains.io.* import java.nio.file.Path import java.nio.file.Paths +import java.util.regex.Pattern + +private val chromeVersionFromUserAgent = Pattern.compile(" Chrome/([\\d.]+) ") private class DefaultWebServerPathHandler : WebServerPathHandler() { override fun process(path: String, @@ -85,10 +90,13 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() { pathToFileManager.pathToInfoCache.put(path, pathInfo) } - // if extraHttpHeaders is not empty, it means that we get request wih token in the query - if (!isSignedRequest && request.origin == null && request.referrer == null && request.isRegularBrowser() && !canBeAccessedDirectly(pathInfo.name)) { - HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request) - return true + val userAgent = request.userAgent + if (!isSignedRequest && userAgent != null && request.isRegularBrowser() && request.origin == null && request.referrer == null) { + val matcher = chromeVersionFromUserAgent.matcher(userAgent) + if (matcher.find() && StringUtil.compareVersionNumbers(matcher.group(1), "51") < 0 && !canBeAccessedDirectly(pathInfo.name)) { + HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request) + return true + } } if (!indexUsed && !endsWithName(path, pathInfo.name)) { @@ -142,4 +150,15 @@ private fun checkAccess(pathInfo: PathInfo, channel: Channel, request: HttpReque } return true +} + +private fun canBeAccessedDirectly(path: String): Boolean { + for (fileHandler in WebServerFileHandler.EP_NAME.extensions) { + for (ext in fileHandler.pageFileExtensions) { + if (FileUtilRt.extensionEquals(path, ext)) { + return true + } + } + } + return false } \ No newline at end of file From 27c5e39ee4689ee327d9983d00fb86a623ed6eb4 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 13 May 2016 18:14:49 +0200 Subject: [PATCH 67/67] WEB-21594 Web preview opens a 404 Not Found document --- .../builtInWebServer/DefaultWebServerPathHandler.kt | 3 ++- .../org/jetbrains/builtInWebServer/StaticFileHandler.kt | 2 +- platform/built-in-server/testSrc/BuiltInServerTestCase.kt | 1 + platform/built-in-server/testSrc/BuiltInWebServerTest.kt | 8 ++++---- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt index 2876c923e9bd..f3b93dceabb6 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt @@ -139,7 +139,8 @@ private fun checkAccess(pathInfo: PathInfo, channel: Channel, request: HttpReque HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request) return false } - else if (!checkAccess(file, Paths.get(pathInfo.root.path))) { + else if (!hasAccess(file)) { + // we check only file, but all directories in the path because of https://youtrack.jetbrains.com/issue/WEB-21594 HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request) return false } diff --git a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt index 457e39c19f5b..b8325a0a26ac 100644 --- a/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt +++ b/platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt @@ -105,4 +105,4 @@ internal fun checkAccess(file: Path, root: Path = file.root): Boolean { } // deny access to any dot prefixed file -private fun hasAccess(result: Path) = Files.isReadable(result) && !(Files.isHidden(result) || result.fileName.toString().startsWith('.')) \ No newline at end of file +internal fun hasAccess(result: Path) = Files.isReadable(result) && !(Files.isHidden(result) || result.fileName.toString().startsWith('.')) \ No newline at end of file diff --git a/platform/built-in-server/testSrc/BuiltInServerTestCase.kt b/platform/built-in-server/testSrc/BuiltInServerTestCase.kt index b30634beb5a4..1e84816f1801 100644 --- a/platform/built-in-server/testSrc/BuiltInServerTestCase.kt +++ b/platform/built-in-server/testSrc/BuiltInServerTestCase.kt @@ -55,6 +55,7 @@ internal abstract class BuiltInServerTestCase { internal fun testUrl(url: String, expectedStatus: HttpResponseStatus): HttpURLConnection { val connection = URL(url).openConnection() as HttpURLConnection + BuiltInServerManager.getInstance().configureRequestToWebServer(connection) assertThat(HttpResponseStatus.valueOf(connection.responseCode)).isEqualTo(expectedStatus) return connection } \ No newline at end of file diff --git a/platform/built-in-server/testSrc/BuiltInWebServerTest.kt b/platform/built-in-server/testSrc/BuiltInWebServerTest.kt index d457fac331be..7ec80123d0de 100644 --- a/platform/built-in-server/testSrc/BuiltInWebServerTest.kt +++ b/platform/built-in-server/testSrc/BuiltInWebServerTest.kt @@ -93,7 +93,7 @@ internal class HeavyBuiltInWebServerTest { } @Test - fun `hidden dir`() { + fun `file in hidden folder`() { val projectDir = tempDirManager.newPath().resolve("foo/bar") val projectDirPath = projectDir.systemIndependentPath createHeavyProject("$projectDirPath/test.ipr").use { project -> @@ -101,15 +101,15 @@ internal class HeavyBuiltInWebServerTest { LocalFileSystem.getInstance().refreshAndFindFileByPath(projectDirPath) createModule(projectDirPath, project) - val dir = projectDir.resolve(".doNotExposeMe") + val dir = projectDir.resolve(".coverage") if (SystemInfo.isWindows) { Files.setAttribute(dir, "dos:hidden", true) } - val path = dir.resolve("foo").write("doNotExposeMe").systemIndependentPath + val path = dir.resolve("foo").write("exposeMe").systemIndependentPath val relativePath = FileUtil.getRelativePath(project.basePath!!, path, '/') val webPath = StringUtil.replace(UrlEscapers.urlPathSegmentEscaper().escape("${project.name}/$relativePath"), "%2F", "/") - testUrl("http://localhost:${BuiltInServerManager.getInstance().port}/$webPath", HttpResponseStatus.FORBIDDEN) + testUrl("http://localhost:${BuiltInServerManager.getInstance().port}/$webPath", HttpResponseStatus.OK) } } } \ No newline at end of file