mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Merge branch 'IDEA-CR-10038'
# Conflicts: # platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java # platform/platform-impl/src/com/intellij/ide/impl/ProjectUtil.java # platform/util/resources/misc/registry.properties
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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())));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<root>
|
||||
<item name='io.netty.resolver.HostsFileEntriesResolver java.net.InetAddress address(java.lang.String)'>
|
||||
<annotation name='org.jetbrains.annotations.Nullable'/>
|
||||
</item>
|
||||
</root>
|
||||
@@ -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,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);
|
||||
}
|
||||
+19
-11
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.jetbrains.builtInWebServer.BuiltInServerConfigurableUi">
|
||||
<grid id="27dc6" binding="mainPanel" layout-manager="GridLayoutManager" row-count="2" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<grid id="27dc6" binding="mainPanel" layout-manager="GridLayoutManager" row-count="3" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<margin top="0" left="0" bottom="0" right="0"/>
|
||||
<constraints>
|
||||
<xy x="20" y="20" width="1077" height="321"/>
|
||||
@@ -8,14 +8,6 @@
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<component id="696ad" class="javax.swing.JLabel">
|
||||
<constraints>
|
||||
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties>
|
||||
<text resource-bundle="messages/XmlBundle" key="setting.value.builtin.server.port.label"/>
|
||||
</properties>
|
||||
</component>
|
||||
<component id="e47e0" class="com.intellij.ui.PortField" binding="builtInServerPort">
|
||||
<constraints>
|
||||
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
|
||||
@@ -30,11 +22,27 @@
|
||||
<text resource-bundle="messages/XmlBundle" key="setting.value.can.accept.external.connections"/>
|
||||
</properties>
|
||||
</component>
|
||||
<vspacer id="c36c4">
|
||||
<component id="2c830" class="javax.swing.JLabel">
|
||||
<constraints>
|
||||
<grid row="1" column="0" row-span="1" col-span="3" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
|
||||
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties>
|
||||
<text resource-bundle="messages/XmlBundle" key="setting.value.builtin.server.port.label"/>
|
||||
</properties>
|
||||
</component>
|
||||
<vspacer id="70613">
|
||||
<constraints>
|
||||
<grid row="2" column="2" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
</vspacer>
|
||||
<component id="15e76" class="javax.swing.JCheckBox" binding="allowUnsignedRequestsCheckBox">
|
||||
<constraints>
|
||||
<grid row="1" column="0" row-span="1" col-span="3" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties>
|
||||
<text resource-bundle="messages/XmlBundle" key="setting.value.builtin.server.allow.unsigned.requests"/>
|
||||
</properties>
|
||||
</component>
|
||||
</children>
|
||||
</grid>
|
||||
</form>
|
||||
|
||||
+4
-1
@@ -11,6 +11,7 @@ class BuiltInServerConfigurableUi implements ConfigurableUi<BuiltInServerOptions
|
||||
|
||||
private PortField builtInServerPort;
|
||||
private JCheckBox builtInServerAvailableExternallyCheckBox;
|
||||
private JCheckBox allowUnsignedRequestsCheckBox;
|
||||
|
||||
public BuiltInServerConfigurableUi() {
|
||||
builtInServerPort.setMin(1024);
|
||||
@@ -25,12 +26,14 @@ class BuiltInServerConfigurableUi implements ConfigurableUi<BuiltInServerOptions
|
||||
@Override
|
||||
public boolean isModified(@NotNull BuiltInServerOptions settings) {
|
||||
return builtInServerPort.getNumber() != settings.builtInServerPort ||
|
||||
builtInServerAvailableExternallyCheckBox.isSelected() != settings.builtInServerAvailableExternally;
|
||||
builtInServerAvailableExternallyCheckBox.isSelected() != settings.builtInServerAvailableExternally ||
|
||||
allowUnsignedRequestsCheckBox.isSelected() != settings.allowUnsignedRequests;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BuiltInServerOptions settings) {
|
||||
boolean builtInServerPortChanged = settings.builtInServerPort != builtInServerPort.getNumber() || settings.builtInServerAvailableExternally != builtInServerAvailableExternallyCheckBox.isSelected();
|
||||
settings.allowUnsignedRequests = allowUnsignedRequestsCheckBox.isSelected();
|
||||
if (builtInServerPortChanged) {
|
||||
settings.builtInServerPort = builtInServerPort.getNumber();
|
||||
settings.builtInServerAvailableExternally = builtInServerAvailableExternallyCheckBox.isSelected();
|
||||
|
||||
@@ -36,6 +36,9 @@ public class BuiltInServerOptions implements PersistentStateComponent<BuiltInSer
|
||||
@Attribute
|
||||
public boolean builtInServerAvailableExternally = false;
|
||||
|
||||
@Attribute
|
||||
public boolean allowUnsignedRequests = false;
|
||||
|
||||
public static BuiltInServerOptions getInstance() {
|
||||
return ServiceManager.getService(BuiltInServerOptions.class);
|
||||
}
|
||||
|
||||
+12
-6
@@ -40,6 +40,11 @@ import java.util.List;
|
||||
public class BuiltInWebBrowserUrlProvider extends WebBrowserUrlProvider implements DumbAware {
|
||||
@NotNull
|
||||
public static List<Url> getUrls(@NotNull VirtualFile file, @NotNull Project project, @Nullable String currentAuthority) {
|
||||
return getUrls(file, project, currentAuthority, true);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<Url> getUrls(@NotNull VirtualFile file, @NotNull Project project, @Nullable String currentAuthority, boolean appendAccessToken) {
|
||||
if (currentAuthority != null && !compareAuthority(currentAuthority)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -53,22 +58,23 @@ public class BuiltInWebBrowserUrlProvider extends WebBrowserUrlProvider implemen
|
||||
String path = info.getPath();
|
||||
|
||||
String authority = currentAuthority == null ? "localhost:" + effectiveBuiltInServerPort : currentAuthority;
|
||||
List<Url> urls = new SmartList<>(Urls.newHttpUrl(authority, '/' + project.getName() + '/' + path));
|
||||
String query = appendAccessToken ? "?" + BuiltInWebServerKt.TOKEN_PARAM_NAME + "=" + BuiltInWebServerKt.acquireToken() : "";
|
||||
List<Url> 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
@@ -117,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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,35 +15,65 @@
|
||||
*/
|
||||
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
|
||||
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
|
||||
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.UriUtil
|
||||
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
|
||||
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 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.host
|
||||
import org.jetbrains.io.send
|
||||
import org.jetbrains.io.*
|
||||
import org.jetbrains.notification.SingletonNotificationManager
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.io.IOException
|
||||
import java.math.BigInteger
|
||||
import java.net.InetAddress
|
||||
import java.net.UnknownHostException
|
||||
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
|
||||
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.web.token"
|
||||
|
||||
private val notificationManager by lazy {
|
||||
SingletonNotificationManager(BuiltInServerManagerImpl.NOTIFICATION_GROUP.value, NotificationType.INFORMATION, null)
|
||||
}
|
||||
|
||||
class BuiltInWebServer : HttpRequestHandler() {
|
||||
override fun isAccessible(request: HttpRequest) = request.isLocalOrigin(onlyAnyOrLoopback = false, hostsOnly = true)
|
||||
|
||||
override fun isSupported(request: FullHttpRequest) = super.isSupported(request) || request.method() == HttpMethod.POST
|
||||
|
||||
override fun process(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): Boolean {
|
||||
@@ -72,12 +102,72 @@ 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()))
|
||||
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
|
||||
val configPath = PathManager.getConfigPath()
|
||||
val file = Paths.get(configPath, IDE_TOKEN_FILE)
|
||||
var token: String? = null
|
||||
if (file.exists()) {
|
||||
try {
|
||||
token = UUID.fromString(file.readText()).toString()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
LOG.warn(e)
|
||||
}
|
||||
}
|
||||
if (token == null) {
|
||||
token = UUID.randomUUID().toString()
|
||||
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
|
||||
// http://stackoverflow.com/questions/8134384/chrome-doesnt-create-cookie-for-domain-localhost-in-broken-https
|
||||
val cookie = DefaultCookie(productName + "-" + Integer.toHexString(configPath.hashCode()), token!!)
|
||||
cookie.isHttpOnly = true
|
||||
cookie.setMaxAge(TimeUnit.DAYS.toSeconds(365 * 10))
|
||||
cookie.setPath("/")
|
||||
cookie
|
||||
}
|
||||
|
||||
// expire after access because we reuse tokens
|
||||
private val tokens = CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.MINUTES).build<String, Boolean>()
|
||||
|
||||
fun acquireToken(): String {
|
||||
var token = tokens.asMap().keys.firstOrNull()
|
||||
if (token == null) {
|
||||
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
|
||||
var isEmptyPath: Boolean
|
||||
val isCustomHost = projectNameAsHost != null
|
||||
@@ -130,15 +220,20 @@ private fun doProcess(request: FullHttpRequest, context: ChannelHandlerContext,
|
||||
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)
|
||||
redirectToDirectory(request, context.channel(), projectName, null)
|
||||
return true
|
||||
}
|
||||
|
||||
val path = toIdeaPath(decodedPath, offset)
|
||||
if (path == null) {
|
||||
HttpResponseStatus.BAD_REQUEST.send(context.channel(), request)
|
||||
HttpResponseStatus.BAD_REQUEST.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(context.channel(), request)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -152,6 +247,62 @@ private fun doProcess(request: FullHttpRequest, context: ChannelHandlerContext,
|
||||
return false
|
||||
}
|
||||
|
||||
internal fun HttpRequest.isSignedRequest(): Boolean {
|
||||
if (BuiltInServerOptions.getInstance().allowUnsignedRequests) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 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() }
|
||||
|
||||
// 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
|
||||
internal fun validateToken(request: HttpRequest, channel: Channel, isSignedRequest: Boolean = request.isSignedRequest()): HttpHeaders? {
|
||||
if (BuiltInServerOptions.getInstance().allowUnsignedRequests) {
|
||||
return EmptyHttpHeaders.INSTANCE
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
return null
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -221,7 +372,9 @@ fun findIndexFile(basedir: Path): Path? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun isOwnHostName(host: String): Boolean {
|
||||
// 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
|
||||
}
|
||||
@@ -237,7 +390,7 @@ fun isOwnHostName(host: String): Boolean {
|
||||
// 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) {
|
||||
catch (ignored: IOException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
+74
-16
@@ -17,16 +17,26 @@ 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
|
||||
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 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,
|
||||
@@ -37,18 +47,17 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() {
|
||||
decodedRawPath: String,
|
||||
isCustomHost: Boolean): Boolean {
|
||||
val channel = context.channel()
|
||||
|
||||
val isSignedRequest = request.isSignedRequest()
|
||||
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) {
|
||||
if (path.isEmpty()) {
|
||||
HttpResponseStatus.NOT_FOUND.send(channel, request, "Index file doesn't exist.")
|
||||
return true
|
||||
}
|
||||
else {
|
||||
return false
|
||||
}
|
||||
HttpResponseStatus.NOT_FOUND.send(channel, request, extraHeaders = extraHeaders)
|
||||
return true
|
||||
}
|
||||
|
||||
pathToFileManager.pathToInfoCache.put(path, pathInfo)
|
||||
@@ -56,11 +65,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) {
|
||||
@@ -71,7 +75,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, 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", extraHeaders)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -80,6 +90,15 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() {
|
||||
pathToFileManager.pathToInfoCache.put(path, pathInfo)
|
||||
}
|
||||
|
||||
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)) {
|
||||
if (endsWithSlash(decodedRawPath)) {
|
||||
indexUsed = true
|
||||
@@ -88,20 +107,59 @@ 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", extraHeaders)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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, 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 = extraHeaders)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
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.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request)
|
||||
return false
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
else if (pathInfo.file!!.`is`(VFileProperty.HIDDEN)) {
|
||||
HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request)
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
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
|
||||
import io.netty.channel.Channel
|
||||
import io.netty.channel.ChannelFutureListener
|
||||
@@ -13,30 +11,30 @@ 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.send
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
private class StaticFileHandler : WebServerFileHandler() {
|
||||
override val pageFileExtensions = arrayOf("html", "htm", "shtml", "stm", "shtm")
|
||||
|
||||
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 = pathInfo.name
|
||||
val nameSequence = ioFile.fileName.toString()
|
||||
//noinspection SpellCheckingInspection
|
||||
if (StringUtilRt.endsWithIgnoreCase(nameSequence, ".shtml") || StringUtilRt.endsWithIgnoreCase(nameSequence, ".stm") || StringUtilRt.endsWithIgnoreCase(nameSequence, ".shtm")) {
|
||||
processSsi(ioFile, PathUtilRt.getParentPath(canonicalPath.toString()), project, request, channel)
|
||||
if (nameSequence.endsWith(".shtml", true) || nameSequence.endsWith(".stm", true) || nameSequence.endsWith(".shtm", true)) {
|
||||
processSsi(ioFile, PathUtilRt.getParentPath(canonicalPath.toString()), project, request, channel, extraHeaders)
|
||||
return true
|
||||
}
|
||||
|
||||
sendIoFile(channel, ioFile, request)
|
||||
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) {
|
||||
@@ -58,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)
|
||||
}
|
||||
@@ -68,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())
|
||||
@@ -94,14 +92,17 @@ private class StaticFileHandler : WebServerFileHandler() {
|
||||
}
|
||||
}
|
||||
|
||||
fun sendIoFile(channel: Channel, ioFile: Path, request: HttpRequest) {
|
||||
if (hasAccess(ioFile)) {
|
||||
FileResponses.sendFile(request, channel, ioFile)
|
||||
}
|
||||
else {
|
||||
HttpResponseStatus.FORBIDDEN.send(channel, request)
|
||||
internal fun checkAccess(file: Path, root: Path = file.root): Boolean {
|
||||
var parent = file
|
||||
do {
|
||||
if (!hasAccess(parent)) {
|
||||
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"))
|
||||
// deny access to any dot prefixed file
|
||||
internal fun hasAccess(result: Path) = Files.isReadable(result) && !(Files.isHidden(result) || result.fileName.toString().startsWith('.'))
|
||||
@@ -19,12 +19,16 @@ 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 {
|
||||
internal val EP_NAME = ExtensionPointName.create<WebServerFileHandler>("org.jetbrains.webServerFileHandler")
|
||||
}
|
||||
|
||||
open val pageFileExtensions: Array<String>
|
||||
get() = emptyArray()
|
||||
|
||||
/**
|
||||
* canonicalRequestPath contains index file name (if not specified in the request)
|
||||
*/
|
||||
@@ -33,8 +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"
|
||||
@@ -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,9 @@ 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, 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())
|
||||
response.send(channel, request)
|
||||
response.send(channel, request, extraHeaders)
|
||||
}
|
||||
@@ -10,13 +10,23 @@ 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.UrlImpl;
|
||||
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.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.URLConnection;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -116,6 +126,25 @@ 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 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());
|
||||
}
|
||||
|
||||
private static void bindCustomPorts(@NotNull BuiltInServer server) {
|
||||
if (ApplicationManager.getApplication().isUnitTestMode()) {
|
||||
return;
|
||||
@@ -130,4 +159,37 @@ 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 = StringUtil.parseInt(authority.substring(portIndex + 1), -1);
|
||||
if (port == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BuiltInServerOptions options = BuiltInServerOptions.getInstance();
|
||||
int idePort = BuiltInServerManager.getInstance().getPort();
|
||||
if (options.builtInServerPort != port && idePort != port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String host = authority.substring(0, portIndex);
|
||||
if (NetUtils.isLocalhost(host)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
InetAddress inetAddress = InetAddress.getByName(host);
|
||||
return inetAddress.isLoopbackAddress() ||
|
||||
inetAddress.isAnyLocalAddress() ||
|
||||
(options.builtInServerAvailableExternally && idePort != port && NetworkInterface.getByInetAddress(inetAddress) != null);
|
||||
}
|
||||
catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -169,4 +170,9 @@ final class DiffHttpService extends RestService {
|
||||
reader.endArray();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccessible(@NotNull HttpRequest request) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,354 +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.ide.IdeBundle;
|
||||
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 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;
|
||||
|
||||
/**
|
||||
* @api {get} /file Open file
|
||||
* @apiName file
|
||||
* @apiGroup Platform
|
||||
*
|
||||
* @apiParam {String} file The path of the file. Relative (to project base dir, VCS root, module source or content root) or absolute.
|
||||
* @apiParam {Integer} [line] The line number of the file (1-based).
|
||||
* @apiParam {Integer} [column] The column number of the file (1-based).
|
||||
* @apiParam {Boolean} [focused=true] Whether to focus project window.
|
||||
*
|
||||
* @apiExample {curl} Absolute path
|
||||
* curl http://localhost:63342/api/file//absolute/path/to/file.kt
|
||||
*
|
||||
* @apiExample {curl} Relative path
|
||||
* curl http://localhost:63342/api/file/relative/to/module/root/path/to/file.kt
|
||||
*
|
||||
* @apiExample {curl} With line and column
|
||||
* curl http://localhost:63342/api/file/relative/to/module/root/path/to/file.kt:100:34
|
||||
*
|
||||
* @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;
|
||||
|
||||
private volatile long refreshSessionId = 0;
|
||||
private final ConcurrentLinkedQueue<OpenFileTask> requests = new ConcurrentLinkedQueue<OpenFileTask>();
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected String getServiceName() {
|
||||
return "file";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isMethodSupported(@NotNull HttpMethod method) {
|
||||
return method == HttpMethod.GET || method == HttpMethod.POST;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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());
|
||||
if (apiRequest.file == null) {
|
||||
apiRequest.file = matcher.group(1).trim();
|
||||
}
|
||||
if (apiRequest.line == -1) {
|
||||
apiRequest.line = StringUtilRt.parseInt(matcher.group(2), 1);
|
||||
}
|
||||
if (apiRequest.column == -1) {
|
||||
apiRequest.column = StringUtilRt.parseInt(matcher.group(3), 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (apiRequest.file == null) {
|
||||
sendStatus(HttpResponseStatus.BAD_REQUEST, keepAlive, channel);
|
||||
return null;
|
||||
}
|
||||
|
||||
openFile(apiRequest)
|
||||
.done(new Consumer<Void>() {
|
||||
@Override
|
||||
public void consume(Void aVoid) {
|
||||
sendStatus(HttpResponseStatus.OK, keepAlive, channel);
|
||||
}
|
||||
})
|
||||
.rejected(new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void consume(Throwable throwable) {
|
||||
if (throwable == NOT_FOUND) {
|
||||
sendStatus(HttpResponseStatus.NOT_FOUND, 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<Void> 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<Promise<Void>> result = new Ref<>();
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (System.currentTimeMillis() - lastTimeRejected < waitUntilNextRequestTimeout) {
|
||||
return;
|
||||
}
|
||||
|
||||
String msg = IdeBundle.message("warning.load.file.from.share", systemIndependentName);
|
||||
String title = IdeBundle.message("title.load.file.from.share");
|
||||
boolean confirmed = com.intellij.ide.impl.ProjectUtil.confirmLoadingFromRemotePath(msg, title);
|
||||
|
||||
if (!confirmed) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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<Void> openAbsolutePath(@NotNull final File file, @NotNull final OpenFileRequest request) {
|
||||
if (!file.exists()) {
|
||||
return Promise.reject(NOT_FOUND);
|
||||
}
|
||||
|
||||
final AsyncPromise<Void> promise = new AsyncPromise<Void>();
|
||||
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);
|
||||
}
|
||||
else {
|
||||
navigate(ProjectUtil.guessProjectForContentFile(virtualFile), virtualFile, request);
|
||||
promise.setResult(null);
|
||||
}
|
||||
}
|
||||
catch (Throwable e) {
|
||||
promise.setError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
private static final class OpenFileTask {
|
||||
final String path;
|
||||
final OpenFileRequest request;
|
||||
|
||||
final AsyncPromise<Void> promise = new AsyncPromise<Void>();
|
||||
|
||||
OpenFileTask(@NotNull String path, @NotNull OpenFileRequest request) {
|
||||
this.path = path;
|
||||
this.request = request;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* 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.ide.impl.ProjectUtil.focusProjectWindow
|
||||
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.builtInWebServer.checkAccess
|
||||
import org.jetbrains.concurrency.AsyncPromise
|
||||
import org.jetbrains.concurrency.Promise
|
||||
import org.jetbrains.concurrency.catchError
|
||||
import org.jetbrains.concurrency.rejectedPromise
|
||||
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
|
||||
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+))?$")
|
||||
|
||||
/**
|
||||
* @api {get} /file Open file
|
||||
* @apiName file
|
||||
* @apiGroup Platform
|
||||
*
|
||||
* @apiParam {String} file The path of the file. Relative (to project base dir, VCS root, module source or content root) or absolute.
|
||||
* @apiParam {Integer} [line] The line number of the file (1-based).
|
||||
* @apiParam {Integer} [column] The column number of the file (1-based).
|
||||
* @apiParam {Boolean} [focused=true] Whether to focus project window.
|
||||
*
|
||||
* @apiExample {curl} Absolute path
|
||||
* curl http://localhost:63342/api/file//absolute/path/to/file.kt
|
||||
*
|
||||
* @apiExample {curl} Relative path
|
||||
* curl http://localhost:63342/api/file/relative/to/module/root/path/to/file.kt
|
||||
*
|
||||
* @apiExample {curl} With line and column
|
||||
* curl http://localhost:63342/api/file/relative/to/module/root/path/to/file.kt:100:34
|
||||
*
|
||||
* @apiExample {curl} Query parameters
|
||||
* curl http://localhost:63342/api/file?file=path/to/file.kt&line=100&column=34
|
||||
*/
|
||||
internal class OpenFileHttpService : RestService() {
|
||||
@Volatile private var refreshSessionId: Long = 0
|
||||
private val requests = ConcurrentLinkedQueue<OpenFileTask>()
|
||||
|
||||
override fun getServiceName() = "file"
|
||||
|
||||
override fun isMethodSupported(method: HttpMethod) = 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()
|
||||
|
||||
val apiRequest: OpenFileRequest
|
||||
if (request.method() === HttpMethod.POST) {
|
||||
apiRequest = gson.value.fromJson(createJsonReader(request), OpenFileRequest::class.java)
|
||||
}
|
||||
else {
|
||||
apiRequest = 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)
|
||||
}
|
||||
|
||||
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)
|
||||
LOG.assertTrue(matcher.matches())
|
||||
if (apiRequest.file == null) {
|
||||
apiRequest.file = matcher.group(1).trim { it <= ' ' }
|
||||
}
|
||||
if (apiRequest.line == -1) {
|
||||
apiRequest.line = StringUtilRt.parseInt(matcher.group(2), 1)
|
||||
}
|
||||
if (apiRequest.column == -1) {
|
||||
apiRequest.column = StringUtilRt.parseInt(matcher.group(3), 1)
|
||||
}
|
||||
}
|
||||
|
||||
if (apiRequest.file == null) {
|
||||
sendStatus(HttpResponseStatus.BAD_REQUEST, keepAlive, channel)
|
||||
return null
|
||||
}
|
||||
|
||||
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
|
||||
sendStatus(HttpResponseStatus.NOT_FOUND.orInSafeMode(HttpResponseStatus.OK), keepAlive, channel)
|
||||
LOG.warn("File ${apiRequest.file} not found")
|
||||
}
|
||||
else {
|
||||
// todo send error
|
||||
sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR, keepAlive, channel)
|
||||
LOG.error(it)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun openFile(request: OpenFileRequest, context: ChannelHandlerContext, httpRequest: HttpRequest?): Promise<Void>? {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
val queue = RefreshQueue.getInstance()
|
||||
queue.cancelSession(refreshSessionId)
|
||||
val mainTask = OpenFileTask(FileUtil.toCanonicalPath(systemIndependentPath, '/'), 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 {
|
||||
task.promise.setError(NOT_FOUND)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, ModalityState.NON_MODAL)
|
||||
|
||||
session.addAllFiles(*ManagingFS.getInstance().localRoots)
|
||||
refreshSessionId = session.id
|
||||
session.launch()
|
||||
return mainTask.promise
|
||||
}
|
||||
|
||||
override fun isAccessible(request: HttpRequest) = true
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
var focused = true
|
||||
}
|
||||
|
||||
private class OpenFileTask(internal val path: String, internal val request: OpenFileRequest) {
|
||||
internal val promise = AsyncPromise<Void>()
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return virtualFile?.let {
|
||||
AppUIUtil.invokeLaterIfProjectAlive(project!!, Runnable { navigate(project, it, request) })
|
||||
true
|
||||
} ?: false
|
||||
}
|
||||
|
||||
private fun openAbsolutePath(file: Path, request: OpenFileRequest): Promise<Void> {
|
||||
val promise = AsyncPromise<Void>()
|
||||
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
|
||||
}
|
||||
@@ -1,47 +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) {
|
||||
OpenFileHttpService.OpenFileRequest request = new OpenFileHttpService.OpenFileRequest();
|
||||
request.file = path;
|
||||
request.line = line;
|
||||
request.column = column;
|
||||
request.focused = false;
|
||||
return HttpRequestHandler.EP_NAME.findExtension(OpenFileHttpService.class).openFile(request).getState() != Promise.State.REJECTED;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -74,4 +75,9 @@ public class ProjectSetRequestHandler extends RestService {
|
||||
sendOk(request, context);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccessible(@NotNull HttpRequest request) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <a href="http://apidocjs.com">apiDoc</a>. 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<InetAddress, AtomicInteger> abuseCounter =
|
||||
CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build(CacheLoader.from((Supplier<AtomicInteger>)AtomicInteger::new));
|
||||
|
||||
private final Cache<String, Boolean> 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 {
|
||||
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;
|
||||
}
|
||||
|
||||
if (!isHostTrusted(request)) {
|
||||
Responses.send(Responses.orInSafeMode(HttpResponseStatus.FORBIDDEN, 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,45 @@ public abstract class RestService extends HttpRequestHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected final void activateLastFocusedFrame() {
|
||||
// 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);
|
||||
}
|
||||
|
||||
String host;
|
||||
try {
|
||||
host = StringUtil.nullize(referrer == null ? null : new URI(referrer).getHost());
|
||||
}
|
||||
catch (URISyntaxException ignored) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Ref<Boolean> 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();
|
||||
@@ -187,12 +263,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, Object> handlerMapping;
|
||||
private final Map<String, Object> handlerMapping = new THashMap<String, Object>();
|
||||
|
||||
public XmlRpcServerImpl() {
|
||||
handlerMapping = new THashMap<String, Object>();
|
||||
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 {
|
||||
@@ -89,48 +75,45 @@ public class XmlRpcServerImpl implements XmlRpcServer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean process(@NotNull String path, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context, @Nullable Map<String, Object> handlers) throws IOException {
|
||||
public boolean process(@NotNull String path, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context, @Nullable Map<String, Object> 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<String, Object> handlers) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -30,12 +29,12 @@ 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) {
|
||||
private val requestIdCounter = AtomicInteger()
|
||||
protected val requests: ConcurrentIntObjectMap<Channel> = ContainerUtil.createConcurrentIntObjectMap<Channel>()
|
||||
private val requests = ContainerUtil.createConcurrentIntObjectMap<ClientInfo>()
|
||||
|
||||
override fun configureBootstrap(bootstrap: Bootstrap, errorOutputConsumer: Consumer<String>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ClientInfo(val channel: Channel, val extraHeaders: HttpHeaders)
|
||||
@@ -10,22 +10,16 @@ import io.netty.util.AttributeKey
|
||||
import org.jetbrains.concurrency.Promise
|
||||
import org.jetbrains.io.webSocket.WebSocketServerOptions
|
||||
|
||||
val CLIENT = AttributeKey.valueOf<Client>("SocketHandler.client")
|
||||
internal val CLIENT = AttributeKey.valueOf<Client>("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<Client>()
|
||||
@@ -94,10 +88,6 @@ class ClientManager(private val listener: ClientListener?, val exceptionHandler:
|
||||
|
||||
fun forEachClient(procedure: TObjectProcedure<Client>) {
|
||||
synchronized (clients) {
|
||||
if (clients.isEmpty) {
|
||||
return
|
||||
}
|
||||
|
||||
clients.forEach(procedure)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 `file in hidden folder`() {
|
||||
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(".coverage")
|
||||
if (SystemInfo.isWindows) {
|
||||
Files.setAttribute(dir, "dos:hidden", true)
|
||||
}
|
||||
|
||||
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.OK)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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", false)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `google name`() {
|
||||
assertThat(isLocalHost("google.com", false)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `jetbrains name`() {
|
||||
assertThat(isLocalHost("jetbrains.com", false)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unknown name`() {
|
||||
assertThat(isLocalHost("foo.com", false)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unknown unqualified name`() {
|
||||
assertThat(isLocalHost("local", false)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid ip`() {
|
||||
assertThat(isLocalHost("0.0.0.0.0.0.0", false)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `any`() {
|
||||
assertThat(isLocalHost("0.0.0.0", false)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `localhost`() {
|
||||
assertThat(isLocalHost("localhost", false)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `localhost only loopback`() {
|
||||
assertThat(isLocalHost("localhost", true)).isTrue()
|
||||
}
|
||||
}
|
||||
@@ -30,5 +30,6 @@
|
||||
<orderEntry type="module" module-name="structure-view-impl" exported="" />
|
||||
<orderEntry type="library" name="commons-logging" level="project" />
|
||||
<orderEntry type="module" module-name="diff-impl" />
|
||||
<orderEntry type="module" module-name="built-in-server-api" />
|
||||
</component>
|
||||
</module>
|
||||
+20
-4
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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<XmlRpcHandlerBean> EP_NAME = ExtensionPointName.create("com.intellij.xmlRpcHandler");
|
||||
|
||||
@Attribute("name")
|
||||
public String name;
|
||||
|
||||
@Attribute("implementation")
|
||||
public String implementation;
|
||||
}
|
||||
@@ -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<String, Object> handlers) throws IOException;
|
||||
boolean process(@NotNull String path, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context, @Nullable Map<String, Object> handlers);
|
||||
|
||||
final class SERVICE {
|
||||
private SERVICE() {
|
||||
|
||||
@@ -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<T extends 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() {
|
||||
|
||||
@@ -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,14 +42,9 @@ public class NetUtils {
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
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) {
|
||||
@@ -224,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,14 +207,21 @@ public class ProjectUtil {
|
||||
return project;
|
||||
}
|
||||
|
||||
public static boolean confirmLoadingFromRemotePath(@NotNull String msg, @NotNull String title) {
|
||||
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 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;
|
||||
}
|
||||
|
||||
private static Window getActiveFrameOrWelcomeScreen() {
|
||||
public static Window getActiveFrameOrWelcomeScreen() {
|
||||
Window window = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow();
|
||||
if (window != null) return window;
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -325,6 +325,10 @@ public final class SocketLock {
|
||||
}
|
||||
|
||||
contentLength = buffer.readUnsignedShort();
|
||||
if (contentLength > 8192) {
|
||||
context.close();
|
||||
return;
|
||||
}
|
||||
myState = State.CONTENT;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -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,10 @@ 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"));
|
||||
FileUtil.delete(new File(dest, "user.web.token"));
|
||||
|
||||
File oldPluginsDir = new File(src, PLUGINS_PATH);
|
||||
if (!oldPluginsDir.isDirectory() && SystemInfo.isMac) {
|
||||
oldPluginsDir = getSettingsPath(oldInstallationHome, settings, PathManager.PROPERTY_PLUGINS_PATH,
|
||||
@@ -340,14 +344,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
|
||||
|
||||
+12
-1
@@ -27,8 +27,10 @@ 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;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -36,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;
|
||||
@@ -56,7 +67,7 @@ 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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
+3
-4
@@ -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<Request extends AbstractRequest, Response exte
|
||||
host = InetAddress.getLocalHost();
|
||||
}
|
||||
catch (UnknownHostException ignored) {
|
||||
host = NetUtils.getLoopbackAddress();
|
||||
host = InetAddress.getLoopbackAddress();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +97,7 @@ public class SocketConnectionImpl<Request extends AbstractRequest, Response exte
|
||||
Exception exception = null;
|
||||
InetAddress host = myHost;
|
||||
if (host == null) {
|
||||
host = NetUtils.getLoopbackAddress();
|
||||
host = InetAddress.getLoopbackAddress();
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < MAX_CONNECTION_ATTEMPTS; attempt++) {
|
||||
|
||||
+15
-2
@@ -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,17 @@ public abstract class HttpRequestHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
@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.
|
||||
// So we should check Host header.
|
||||
return host != null && NettyKt.isLocalOrigin(request) && NettyKt.parseAndCheckIsLocalHost("http://" + host);
|
||||
}
|
||||
|
||||
public boolean isSupported(@NotNull FullHttpRequest request) {
|
||||
return request.method() == HttpMethod.GET || request.method() == HttpMethod.HEAD;
|
||||
}
|
||||
@@ -50,4 +63,4 @@ public abstract class HttpRequestHandler {
|
||||
*/
|
||||
public abstract boolean process(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context)
|
||||
throws IOException;
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import com.intellij.util.ArrayUtil;
|
||||
import com.intellij.util.ExceptionUtil;
|
||||
import com.intellij.util.NotNullProducer;
|
||||
import com.intellij.util.SystemProperties;
|
||||
import com.intellij.util.net.NetUtils;
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
@@ -140,7 +139,7 @@ public class BuiltInServer implements Disposable {
|
||||
boolean tryAnyPort,
|
||||
@NotNull ServerBootstrap bootstrap,
|
||||
@NotNull ChannelRegistrar channelRegistrar) throws Exception {
|
||||
InetAddress address = NetUtils.getLoopbackAddress();
|
||||
InetAddress address = InetAddress.getLoopbackAddress();
|
||||
|
||||
for (int i = 0; i < portsCount; i++) {
|
||||
int port = firstPort + i;
|
||||
|
||||
@@ -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.
|
||||
@@ -28,7 +28,6 @@ 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
|
||||
|
||||
private val PREV_HANDLER = AttributeKey.valueOf<HttpRequestHandler>("DelegatingHttpRequestHandler.handler")
|
||||
@@ -38,10 +37,14 @@ internal class DelegatingHttpRequestHandler : DelegatingHttpRequestHandlerBase()
|
||||
override fun process(context: ChannelHandlerContext,
|
||||
request: FullHttpRequest,
|
||||
urlDecoder: QueryStringDecoder): Boolean {
|
||||
fun HttpRequestHandler.checkAndProcess(): Boolean {
|
||||
return isSupported(request) && !request.isWriteFromBrowserWithoutOrigin() && isAccessible(request) && 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.checkAndProcess()) {
|
||||
return true
|
||||
}
|
||||
// prev cached connectedHandler is not suitable for this request, so, let's find it again
|
||||
@@ -50,7 +53,7 @@ internal class DelegatingHttpRequestHandler : DelegatingHttpRequestHandlerBase()
|
||||
|
||||
for (handler in HttpRequestHandler.EP_NAME.extensions) {
|
||||
try {
|
||||
if (handler.isSupported(request) && handler.process(urlDecoder, request, context)) {
|
||||
if (handler.checkAndProcess()) {
|
||||
prevHandlerAttribute.set(handler)
|
||||
return true
|
||||
}
|
||||
@@ -58,7 +61,6 @@ internal class DelegatingHttpRequestHandler : DelegatingHttpRequestHandlerBase()
|
||||
catch (e: Throwable) {
|
||||
Logger.getInstance(BuiltInServer::class.java).error(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (urlDecoder.path() == "/favicon.ico") {
|
||||
|
||||
@@ -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<FullHttpRequest>() {
|
||||
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 { "\n\nIN HTTP: $message\n\n" }
|
||||
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,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;
|
||||
@@ -30,6 +28,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;
|
||||
@@ -199,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));
|
||||
}
|
||||
@@ -234,8 +217,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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -32,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)
|
||||
@@ -78,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))
|
||||
}
|
||||
|
||||
@@ -99,6 +102,11 @@ fun HttpResponse.addKeepAliveIfNeed(request: HttpRequest): Boolean {
|
||||
fun HttpResponse.addCommonHeaders() {
|
||||
addServer()
|
||||
setDate()
|
||||
if (!headers().contains("X-Frame-Options")) {
|
||||
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) {
|
||||
@@ -116,14 +124,25 @@ 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.http.server.response.actual.status", 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) {
|
||||
return responseStatus.response()
|
||||
return DefaultFullHttpResponse(HttpVersion.HTTP_1_1, responseStatus, Unpooled.EMPTY_BUFFER)
|
||||
}
|
||||
|
||||
val builder = StringBuilder()
|
||||
|
||||
@@ -15,8 +15,12 @@
|
||||
*/
|
||||
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
|
||||
import io.netty.buffer.ByteBuf
|
||||
@@ -27,12 +31,17 @@ 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
|
||||
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.NetworkInterface
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
inline fun Bootstrap.handler(crossinline task: (Channel) -> Unit): Bootstrap {
|
||||
@@ -59,7 +68,7 @@ fun oioClientBootstrap(): Bootstrap {
|
||||
}
|
||||
|
||||
inline fun ChannelFuture.addChannelListener(crossinline listener: (future: ChannelFuture) -> Unit) {
|
||||
addListener(GenericFutureListener<ChannelFuture> { listener(it) })
|
||||
addListener(GenericFutureListener<io.netty.channel.ChannelFuture> { listener(it) })
|
||||
}
|
||||
|
||||
// if NIO, so, it is shared and we must not shutdown it
|
||||
@@ -98,6 +107,15 @@ val Channel.uriScheme: String
|
||||
val HttpRequest.host: String?
|
||||
get() = headers().getAsString(HttpHeaderNames.HOST)
|
||||
|
||||
val HttpRequest.origin: String?
|
||||
get() = headers().getAsString(HttpHeaderNames.ORIGIN)
|
||||
|
||||
val HttpRequest.referrer: String?
|
||||
get() = headers().getAsString(HttpHeaderNames.REFERER)
|
||||
|
||||
val HttpRequest.userAgent: String?
|
||||
get() = headers().getAsString(HttpHeaderNames.USER_AGENT)
|
||||
|
||||
inline fun <T> ByteBuf.releaseIfError(task: () -> T): T {
|
||||
try {
|
||||
return task()
|
||||
@@ -110,4 +128,72 @@ inline fun <T> ByteBuf.releaseIfError(task: () -> T): T {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isLocalHost(host: String, onlyAnyOrLoopback: Boolean, hostsOnly: Boolean = false): Boolean {
|
||||
if (NetUtils.isLocalhost(host)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
try {
|
||||
val address = InetAddress.getByName(host)
|
||||
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() }
|
||||
}
|
||||
else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
catch (ignored: IOException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun HttpRequest.isLocalOrigin(onlyAnyOrLoopback: Boolean = true, hostsOnly: Boolean = false) = parseAndCheckIsLocalHost(origin, onlyAnyOrLoopback, hostsOnly) && parseAndCheckIsLocalHost(referrer, onlyAnyOrLoopback, hostsOnly)
|
||||
|
||||
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) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
val parsedUri = Urls.parse(uri, false) ?: return false
|
||||
val host = parsedUri.host
|
||||
return host != null && (isTrustedChromeExtension(parsedUri) || isLocalHost(host, onlyAnyOrLoopback, hostsOnly))
|
||||
}
|
||||
catch (ignored: Exception) {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun HttpRequest.isRegularBrowser() = userAgent?.startsWith("Mozilla/5.0") ?: false
|
||||
|
||||
// forbid POST requests from browser without Origin
|
||||
fun HttpRequest.isWriteFromBrowserWithoutOrigin(): Boolean {
|
||||
val method = method()
|
||||
return origin.isNullOrEmpty() && isRegularBrowser() && (method == HttpMethod.POST || method == HttpMethod.PATCH || method == HttpMethod.PUT || method == HttpMethod.DELETE)
|
||||
}
|
||||
@@ -1197,4 +1197,7 @@ 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
|
||||
|
||||
change.color.command.text=Change color
|
||||
change.color.command.text=Change color
|
||||
|
||||
warning.use.rest.api=''{0}'' API is requested. Do you trust ''{1}''?
|
||||
title.use.rest.api=Using REST API
|
||||
@@ -250,4 +250,5 @@ javascript.debugger.settings.choose.file.subtitle=Select file to debug JavaScrip
|
||||
|
||||
setting.builtin.server.category.label=Built-in server
|
||||
setting.value.builtin.server.port.label=&Port\:
|
||||
setting.value.can.accept.external.connections=Can accept &external connections
|
||||
setting.value.can.accept.external.connections=Can accept &external connections
|
||||
setting.value.builtin.server.allow.unsigned.requests=&Allow unsigned requests
|
||||
|
||||
@@ -192,9 +192,6 @@
|
||||
|
||||
<extensionPoint name="statistics.usagesCollector" interface="com.intellij.internal.statistic.UsagesCollector"/>
|
||||
|
||||
<!--suppress ExtensionPointBeanClass -->
|
||||
<extensionPoint name="xmlRpcHandler" beanClass="com.intellij.ide.XmlRpcHandlerBean"/>
|
||||
|
||||
<extensionPoint name="editorHighlighterProvider" beanClass="com.intellij.openapi.fileTypes.FileTypeExtensionPoint">
|
||||
<with attribute="implementationClass" implements="com.intellij.openapi.fileTypes.EditorHighlighterProvider"/>
|
||||
</extensionPoint>
|
||||
@@ -217,11 +214,11 @@
|
||||
<extensionPoint name="colorPickerListenerFactory" interface="com.intellij.ui.ColorPickerListenerFactory"/>
|
||||
|
||||
<extensionPoint name="search.topHitProvider" interface="com.intellij.ide.SearchTopHitProvider"/>
|
||||
|
||||
|
||||
<extensionPoint name="search.optionContributor" interface="com.intellij.ide.ui.search.SearchableOptionContributor"/>
|
||||
|
||||
<extensionPoint name="ui.suitableFontProvider" interface="com.intellij.ui.SuitableFontProvider"/>
|
||||
|
||||
|
||||
<extensionPoint name="schemeImporter" beanClass="com.intellij.openapi.options.SchemeImporterEP">
|
||||
<with attribute="schemeClass" implements="com.intellij.openapi.options.Scheme"/>
|
||||
<with attribute="implementationClass" implements="com.intellij.openapi.options.SchemeImporter"/>
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
|
||||
<applicationService serviceInterface="com.intellij.ide.XmlRpcServer" serviceImplementation="org.jetbrains.ide.XmlRpcServerImpl"/>
|
||||
|
||||
<xmlRpcHandler name="fileOpener" implementation="org.jetbrains.ide.OpenFileXmlRpcHandler"/>
|
||||
|
||||
<httpRequestHandler implementation="org.jetbrains.ide.XmlRpcServerImpl$XmlRpcRequestHandler"/>
|
||||
<httpRequestHandler implementation="org.jetbrains.ide.ProjectSetRequestHandler"/>
|
||||
<httpRequestHandler implementation="org.jetbrains.ide.DiffHttpService"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -740,4 +740,8 @@ editor.rainbow.identifiers=false
|
||||
editor.rainbow.identifiers.description=Rainbow identifiers in editor
|
||||
|
||||
ide.intellij.laf.win10.ui=false
|
||||
ide.intellij.laf.win10.ui.description=Enables Windows 10 look
|
||||
ide.intellij.laf.win10.ui.description=Enables Windows 10 look
|
||||
|
||||
ide.http.server.response.actual.status=false
|
||||
ide.rest.api.requests.per.minute=30
|
||||
ide.built.in.web.server.activatable=false
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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 = "intellij-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<String> replyToChallenge(final int handlerNo,
|
||||
Vector<String> 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);
|
||||
}
|
||||
|
||||
@@ -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<String> replyToChallenge(final int handlerNo,
|
||||
public Vector<String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<OutputStream> 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;
|
||||
|
||||
@@ -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 GitXmlRpcHandlerService<GitHttp
|
||||
public class InternalRequestHandlerDelegate implements GitAskPassXmlRpcHandler {
|
||||
@NotNull
|
||||
@Override
|
||||
public String askUsername(int handler, @NotNull String url) {
|
||||
return getHandler(handler).askUsername(url);
|
||||
public String askUsername(String token, @NotNull String url) {
|
||||
return getHandler(UUID.fromString(token)).askUsername(url);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String askPassword(int handler, @NotNull String url) {
|
||||
return getHandler(handler).askPassword(url);
|
||||
public String askPassword(String token, @NotNull String url) {
|
||||
return getHandler(UUID.fromString(token)).askPassword(url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2000-2009 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.
|
||||
@@ -27,6 +27,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.git4idea.util.ScriptGenerator;
|
||||
import org.jetbrains.ide.BuiltInServerManager;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
@@ -53,7 +54,7 @@ public class GitRebaseEditorService {
|
||||
/**
|
||||
* Random number generator
|
||||
*/
|
||||
private static final Random oursRandom = new Random();
|
||||
private static final Random oursRandom = new SecureRandom();
|
||||
/**
|
||||
* The prefix for rebase editors
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
@@ -30,6 +30,7 @@ import org.jetbrains.ide.BuiltInServerManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* <p>The provider of external application scripts called by Git when a remote operation needs communication with the user.</p>
|
||||
@@ -57,8 +58,7 @@ public abstract class GitXmlRpcHandlerService<T> {
|
||||
@Nullable private File myScriptPath;
|
||||
@NotNull private final Object SCRIPT_FILE_LOCK = new Object();
|
||||
|
||||
@NotNull private final THashMap<Integer, T> handlers = new THashMap<Integer, T>();
|
||||
private int myNextHandlerKey;
|
||||
@NotNull private final THashMap<UUID, T> handlers = new THashMap<UUID, T>();
|
||||
@NotNull private final Object HANDLERS_LOCK = new Object();
|
||||
|
||||
/**
|
||||
@@ -111,14 +111,14 @@ public abstract class GitXmlRpcHandlerService<T> {
|
||||
* @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<T> {
|
||||
handlers.remove(key);
|
||||
}
|
||||
});
|
||||
myNextHandlerKey++;
|
||||
return key;
|
||||
}
|
||||
}
|
||||
@@ -146,7 +145,7 @@ public abstract class GitXmlRpcHandlerService<T> {
|
||||
* @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<T> {
|
||||
*
|
||||
* @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");
|
||||
|
||||
@@ -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<GitSSHGUIHandle
|
||||
public class InternalRequestHandler implements GitSSHHandler {
|
||||
|
||||
@Override
|
||||
public boolean verifyServerHostKey(int handler, String hostname, int port, String serverHostKeyAlgorithm, String serverHostKey,
|
||||
public boolean verifyServerHostKey(String handler, String hostname, int port, String serverHostKeyAlgorithm, String serverHostKey,
|
||||
boolean isNew) {
|
||||
return getHandler(handler).verifyServerHostKey(hostname, port, serverHostKeyAlgorithm, serverHostKey, isNew);
|
||||
return getHandler(UUID.fromString(handler)).verifyServerHostKey(hostname, port, serverHostKeyAlgorithm, serverHostKey, isNew);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String askPassphrase(int handler, String username, String keyPath, boolean resetPassword, String lastError) {
|
||||
return adjustNull(getHandler(handler).askPassphrase(username, keyPath, resetPassword, lastError));
|
||||
public String askPassphrase(String handler, String username, String keyPath, boolean resetPassword, String lastError) {
|
||||
return adjustNull(getHandler(UUID.fromString(handler)).askPassphrase(username, keyPath, resetPassword, lastError));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"UseOfObsoleteCollectionType"})
|
||||
public Vector<String> replyToChallenge(int handlerNo, String username, String name, String instruction, int numPrompts,
|
||||
public Vector<String> replyToChallenge(String token, String username, String name, String instruction, int numPrompts,
|
||||
Vector<String> prompt, Vector<Boolean> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,6 @@ import com.intellij.execution.process.ProcessOutputTypes;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.util.Key;
|
||||
import com.intellij.openapi.vfs.CharsetToolkit;
|
||||
import com.intellij.util.net.NetUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -30,6 +29,7 @@ import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
|
||||
@@ -95,7 +95,7 @@ class OutputTabAdapter extends ProcessAdapter {
|
||||
@Nullable
|
||||
private InputStream connect(int port) throws IOException {
|
||||
final long s = System.currentTimeMillis();
|
||||
final InetSocketAddress endpoint = new InetSocketAddress(NetUtils.getLoopbackAddress(), port);
|
||||
final InetSocketAddress endpoint = new InetSocketAddress(InetAddress.getLoopbackAddress(), port);
|
||||
|
||||
myStartedProcess.notifyTextAvailable("Connecting to XSLT runner on " + endpoint + "\n", ProcessOutputTypes.SYSTEM);
|
||||
|
||||
|
||||
@@ -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,42 @@ 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 org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.ide.BuiltInServerManager;
|
||||
|
||||
import java.net.URI;
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void browseUsingNotSystemDefaultBrowserPolicy(@NotNull URI uri, @NotNull GeneralSettings settings, @Nullable Project project) {
|
||||
WebBrowserManager browserManager = WebBrowserManager.getInstance();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Url> getDebuggableUrls(@Nullable PsiElement context) {
|
||||
try {
|
||||
OpenInBrowserRequest request = context == null ? null : OpenInBrowserRequest.create(context);
|
||||
return request == null || request.getFile().getViewProvider().getBaseLanguage() == XMLLanguage.INSTANCE ? Collections.<Url>emptyList() : getUrls(getProvider(request), request);
|
||||
if (request == null || request.getFile().getViewProvider().getBaseLanguage() == XMLLanguage.INSTANCE) {
|
||||
return Collections.<Url>emptyList();
|
||||
}
|
||||
else {
|
||||
// it is client responsibility to set token
|
||||
request.setAppendAccessToken(false);
|
||||
return getUrls(getProvider(request), request);
|
||||
}
|
||||
}
|
||||
catch (WebBrowserUrlProvider.BrowserException ignored) {
|
||||
return Collections.emptyList();
|
||||
|
||||
+2
-2
@@ -23,11 +23,11 @@
|
||||
<orderEntry type="module" module-name="xml-structure-view-impl" exported="" />
|
||||
<orderEntry type="library" name="Netty" level="project" />
|
||||
<orderEntry type="module" module-name="xdebugger-api" />
|
||||
<orderEntry type="module" module-name="built-in-server-api" />
|
||||
</component>
|
||||
<component name="copyright">
|
||||
<Base>
|
||||
<setting name="state" value="1" />
|
||||
</Base>
|
||||
</component>
|
||||
</module>
|
||||
|
||||
</module>
|
||||
@@ -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<Url> 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<Url> getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean isAppendAccessToken() {
|
||||
return appendAccessToken;
|
||||
}
|
||||
|
||||
public void setAppendAccessToken(boolean value) {
|
||||
this.appendAccessToken = value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user