diff --git a/platform/built-in-server/src/org/jetbrains/ide/AboutHttpService.kt b/platform/built-in-server/src/org/jetbrains/ide/AboutHttpService.kt index 3d32d2d3c747..903dd5e0bcf8 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/AboutHttpService.kt +++ b/platform/built-in-server/src/org/jetbrains/ide/AboutHttpService.kt @@ -10,6 +10,7 @@ import com.intellij.openapi.application.ex.ApplicationInfoEx import com.intellij.openapi.fileTypes.FileTypeRegistry import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream import com.intellij.util.PlatformUtils +import com.intellij.util.io.isLocalOrigin import com.intellij.util.io.jackson.array import com.intellij.util.io.jackson.obj import com.intellij.util.io.origin @@ -47,58 +48,61 @@ import java.io.OutputStream internal class AboutHttpService : RestService() { override fun getServiceName() = "about" - override fun isHostTrusted(request: FullHttpRequest, urlDecoder: QueryStringDecoder): Boolean { - return isTrustedOrigin(request) || super.isHostTrusted(request, urlDecoder) - } - - override fun isAccessible(request: HttpRequest): Boolean { - return isTrustedOrigin(request) || super.isAccessible(request) + override fun isOriginAllowed(request: HttpRequest): OriginCheckResult { + val originAllowed = super.isOriginAllowed(request) + if (originAllowed == OriginCheckResult.FORBID) { + val origin = request.origin ?: return OriginCheckResult.FORBID + @Suppress("SpellCheckingInspection") + return if (origin.matches(Regex("https://([a-z0-9-]+\\.)*hyperskill.org$"))) OriginCheckResult.ALLOW else OriginCheckResult.FORBID + } + return originAllowed } override fun execute(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): String? { val byteOut = BufferExposingByteArrayOutputStream() - writeApplicationInfoJson(byteOut, urlDecoder) + writeApplicationInfoJson(byteOut, urlDecoder, request.isLocalOrigin()) send(byteOut, request, context) return null } } -private fun isTrustedOrigin(request: HttpRequest): Boolean { - val origin = request.origin ?: return false - @Suppress("SpellCheckingInspection") - return origin.matches("https?://([a-z0-9-]+\\.)*hyperskill.org$".toRegex()) -} +fun writeApplicationInfoJson(out: OutputStream, urlDecoder: QueryStringDecoder?, isLocalOrigin: Boolean) { + JsonFactory().createGenerator(out).useDefaultPrettyPrinter().use { writer -> + writer.obj { + writeAboutJson(writer) -fun writeApplicationInfoJson(out: OutputStream, urlDecoder: QueryStringDecoder?) { - val writer = JsonFactory().createGenerator(out).useDefaultPrettyPrinter() - writer.obj { - writeAboutJson(writer) - if (urlDecoder != null && getBooleanParameter("registeredFileTypes", urlDecoder)) { - writer.array("registeredFileTypes") { - for (fileType in FileTypeRegistry.getInstance().registeredFileTypes) { - writer.obj { - writer.writeStringField("name", fileType.name) - writer.writeStringField("description", fileType.description) - writer.writeBooleanField("isBinary", fileType.isBinary) + // registeredFileTypes and more args are supported only for explicitly trusted origins + if (!isLocalOrigin) { + return + } + + if (urlDecoder != null && getBooleanParameter("registeredFileTypes", urlDecoder)) { + writer.array("registeredFileTypes") { + for (fileType in FileTypeRegistry.getInstance().registeredFileTypes) { + writer.obj { + writer.writeStringField("name", fileType.name) + writer.writeStringField("description", fileType.description) + writer.writeBooleanField("isBinary", fileType.isBinary) + } } } } - } - if (urlDecoder != null && getBooleanParameter("more", urlDecoder)) { - val appInfo = ApplicationInfoEx.getInstanceEx() - writer.writeStringField("vendor", appInfo.companyName) - writer.writeBooleanField("isEAP", appInfo.isEAP) - writer.writeStringField("productCode", appInfo.build.productCode) - writer.writeNumberField("buildDate", appInfo.buildDate.time.time) - writer.writeBooleanField("isSnapshot", appInfo.build.isSnapshot) - writer.writeStringField("configPath", PathManager.getConfigPath()) - writer.writeStringField("systemPath", PathManager.getSystemPath()) - writer.writeStringField("binPath", PathManager.getBinPath()) - writer.writeStringField("logPath", PathManager.getLogPath()) - writer.writeStringField("homePath", PathManager.getHomePath()) + + if (urlDecoder != null && getBooleanParameter("more", urlDecoder)) { + val appInfo = ApplicationInfoEx.getInstanceEx() + writer.writeStringField("vendor", appInfo.companyName) + writer.writeBooleanField("isEAP", appInfo.isEAP) + writer.writeStringField("productCode", appInfo.build.productCode) + writer.writeNumberField("buildDate", appInfo.buildDate.time.time) + writer.writeBooleanField("isSnapshot", appInfo.build.isSnapshot) + writer.writeStringField("configPath", PathManager.getConfigPath()) + writer.writeStringField("systemPath", PathManager.getSystemPath()) + writer.writeStringField("binPath", PathManager.getBinPath()) + writer.writeStringField("logPath", PathManager.getLogPath()) + writer.writeStringField("homePath", PathManager.getHomePath()) + } } } - writer.close() } fun writeAboutJson(writer: JsonGenerator) { 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 37a36d5c7db2..d7683524bf38 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/DiffHttpService.java +++ b/platform/built-in-server/src/org/jetbrains/ide/DiffHttpService.java @@ -1,18 +1,4 @@ -/* - * 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. - */ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package org.jetbrains.ide; import com.google.gson.stream.JsonReader; @@ -169,7 +155,7 @@ final class DiffHttpService extends RestService { } @Override - public boolean isAccessible(@NotNull HttpRequest request) { - return true; + protected @NotNull OriginCheckResult isOriginAllowed(@NotNull HttpRequest request) { + return OriginCheckResult.ASK_CONFIRMATION; } } diff --git a/platform/built-in-server/src/org/jetbrains/ide/InstallPluginService.kt b/platform/built-in-server/src/org/jetbrains/ide/InstallPluginService.kt index 2c3ff4c2d6be..57e3ce50ba0a 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/InstallPluginService.kt +++ b/platform/built-in-server/src/org/jetbrains/ide/InstallPluginService.kt @@ -28,7 +28,7 @@ import java.net.URISyntaxException internal class InstallPluginService : RestService() { override fun getServiceName() = "installPlugin" - override fun isAccessible(request: HttpRequest) = true + override fun isOriginAllowed(request: HttpRequest) = OriginCheckResult.ASK_CONFIRMATION var isAvailable = 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 c5b8de9f869b..50e25c711492 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt +++ b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt @@ -65,6 +65,8 @@ internal class OpenFileHttpService : RestService() { override fun isMethodSupported(method: HttpMethod) = method === HttpMethod.GET || method === HttpMethod.POST + override fun isOriginAllowed(request: HttpRequest) = OriginCheckResult.ASK_CONFIRMATION + override fun execute(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): String? { val keepAlive = HttpUtil.isKeepAlive(request) val channel = context.channel() @@ -170,8 +172,6 @@ internal class OpenFileHttpService : RestService() { session.launch() return mainTask.promise } - - 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 915325d18f06..8099e71db90c 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.java +++ b/platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.java @@ -59,7 +59,7 @@ final class ProjectSetRequestHandler extends RestService { } @Override - public boolean isAccessible(@NotNull HttpRequest request) { - return true; + protected @NotNull OriginCheckResult isOriginAllowed(@NotNull HttpRequest request) { + return OriginCheckResult.ASK_CONFIRMATION; } } diff --git a/platform/built-in-server/src/org/jetbrains/ide/RestService.kt b/platform/built-in-server/src/org/jetbrains/ide/RestService.kt index ee17d1742c4a..569a240d763f 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/RestService.kt +++ b/platform/built-in-server/src/org/jetbrains/ide/RestService.kt @@ -244,7 +244,7 @@ abstract class RestService : HttpRequestHandler() { @Throws(InterruptedException::class, InvocationTargetException::class) // e.g. upsource trust to configured host protected open fun isHostTrusted(request: FullHttpRequest): Boolean { - if (request.isSignedRequest()) { + if (request.isSignedRequest() || isOriginAllowed(request) == OriginCheckResult.ALLOW) { return true } diff --git a/platform/built-in-server/src/org/jetbrains/ide/StartUpMeasurementService.kt b/platform/built-in-server/src/org/jetbrains/ide/StartUpMeasurementService.kt index f0a0fadd1b2f..2ecf3d5fe454 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/StartUpMeasurementService.kt +++ b/platform/built-in-server/src/org/jetbrains/ide/StartUpMeasurementService.kt @@ -2,12 +2,7 @@ package org.jetbrains.ide import com.intellij.diagnostic.StartUpPerformanceService -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.ex.ApplicationInfoEx -import com.intellij.openapi.diagnostic.logger -import com.intellij.util.io.getHostName import com.intellij.util.io.origin -import com.intellij.util.net.NetUtils import io.netty.buffer.Unpooled import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.FullHttpRequest @@ -15,24 +10,11 @@ import io.netty.handler.codec.http.HttpRequest import io.netty.handler.codec.http.QueryStringDecoder import org.jetbrains.io.response -private val LOG = logger() - internal class StartUpMeasurementService : RestService() { override fun getServiceName() = "startUpMeasurement" - override fun isAccessible(request: HttpRequest): Boolean { - if (super.isAccessible(request)) { - return true - } - - // expose externally to use visualizer front-end - // personal data is not exposed (but someone can say that 3rd plugin class names should be not exposed), - // so, limit to dev builds only (EAP builds are not allowed too) or app in an internal mode (and still only for known hosts) - return isTrustedHostName(request) && (ApplicationManager.getApplication().isInternal || ApplicationInfoEx.getInstanceEx().build.isSnapshot) - } - - override fun isHostTrusted(request: FullHttpRequest, urlDecoder: QueryStringDecoder): Boolean { - return isTrustedHostName(request) || super.isHostTrusted(request, urlDecoder) + override fun isOriginAllowed(request: HttpRequest): OriginCheckResult { + return if (request.origin == "https://ij-perf.jetbrains.com") OriginCheckResult.ALLOW else super.isOriginAllowed(request) } override fun execute(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): String? { @@ -42,12 +24,4 @@ internal class StartUpMeasurementService : RestService() { sendResponse(request, context, response) return null } -} - -private fun isTrustedHostName(request: HttpRequest): Boolean { - val hostName = getHostName(request) ?: return false - if (!NetUtils.isLocalhost(hostName)) { - LOG.error("Expected 'request.hostName' to be localhost. hostName=$hostName, origin=${request.origin}") - } - return hostName == "ij-perf.jetbrains.com" || hostName == "ij-perf.develar.org" || NetUtils.isLocalhost(hostName) } \ No newline at end of file diff --git a/platform/platform-util-io/src/org/jetbrains/ide/HttpRequestHandler.kt b/platform/platform-util-io/src/org/jetbrains/ide/HttpRequestHandler.kt index 934d12670f79..ead346b5106a 100644 --- a/platform/platform-util-io/src/org/jetbrains/ide/HttpRequestHandler.kt +++ b/platform/platform-util-io/src/org/jetbrains/ide/HttpRequestHandler.kt @@ -18,6 +18,12 @@ import java.io.IOException import java.util.* abstract class HttpRequestHandler { + enum class OriginCheckResult { + ALLOW, FORBID, + // any origin is allowed but user confirmation is required + ASK_CONFIRMATION + } + companion object { // Your handler will be instantiated on first user request val EP_NAME = ExtensionPointName("com.intellij.httpRequestHandler") @@ -45,10 +51,12 @@ abstract class HttpRequestHandler { val hostName = getHostName(request) // If attacker.com DNS rebound to 127.0.0.1 and user open site directly - no Origin or Referrer headers. // So we should check Host header. - return hostName != null && isOriginAllowed(request) && isLocalHost(hostName) + return hostName != null && isOriginAllowed(request) != OriginCheckResult.FORBID && isLocalHost(hostName) } - protected open fun isOriginAllowed(request: HttpRequest) = request.isLocalOrigin() + protected open fun isOriginAllowed(request: HttpRequest): OriginCheckResult { + return if (request.isLocalOrigin()) OriginCheckResult.ALLOW else OriginCheckResult.FORBID + } open fun isSupported(request: FullHttpRequest): Boolean { return request.method() === HttpMethod.GET || request.method() === HttpMethod.HEAD