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:
Vladimir Krivosheev
2016-05-17 11:12:11 +02:00
71 changed files with 1281 additions and 802 deletions
@@ -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);
}
@@ -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>
@@ -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);
}
@@ -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
}
}
@@ -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()
}
}
+1
View File
@@ -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>
@@ -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
@@ -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);
@@ -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++) {
@@ -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
View File
@@ -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;
}
}