Merge remote-tracking branch 'origin/master' into IDEA-CR-10038
authorVladimir Krivosheev <vladimir.krivosheev@jetbrains.com>
Fri, 29 Apr 2016 10:54:16 +0000 (12:54 +0200)
committerVladimir Krivosheev <vladimir.krivosheev@jetbrains.com>
Fri, 29 Apr 2016 10:54:16 +0000 (12:54 +0200)
63 files changed:
java/compiler/impl/src/com/intellij/compiler/server/BuildManager.java
java/execution/impl/src/com/intellij/execution/runners/ProcessProxyImpl.java
lib/annotations/netty/io/netty/resolver/annotations.xml [new file with mode: 0644]
platform/built-in-server-api/src/org/jetbrains/ide/BuiltInServerManager.java
platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebBrowserUrlProvider.java
platform/built-in-server/src/org/jetbrains/builtInWebServer/BuiltInWebServer.kt
platform/built-in-server/src/org/jetbrains/builtInWebServer/DefaultWebServerPathHandler.kt
platform/built-in-server/src/org/jetbrains/builtInWebServer/StaticFileHandler.kt
platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerFileHandler.kt
platform/built-in-server/src/org/jetbrains/builtInWebServer/WebServerPathHandler.kt
platform/built-in-server/src/org/jetbrains/ide/BuiltInServerManagerImpl.java
platform/built-in-server/src/org/jetbrains/ide/DiffHttpService.java
platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java [deleted file]
platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt [new file with mode: 0644]
platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java [deleted file]
platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.java
platform/built-in-server/src/org/jetbrains/ide/RestService.java
platform/built-in-server/src/org/jetbrains/ide/XmlRpcServerImpl.java
platform/built-in-server/src/org/jetbrains/io/SubServer.java
platform/built-in-server/src/org/jetbrains/io/fastCgi/FastCgiService.kt
platform/built-in-server/src/org/jetbrains/io/jsonRpc/ClientManager.kt
platform/built-in-server/testSrc/BuiltInWebServerTest.kt
platform/built-in-server/testSrc/IsLocalHostTest.kt [new file with mode: 0644]
platform/platform-api/src/com/intellij/ide/XmlRpcHandlerBean.java [deleted file]
platform/platform-api/src/com/intellij/ide/XmlRpcServer.java
platform/platform-api/src/com/intellij/openapi/ui/MessageDialogBuilder.java
platform/platform-api/src/com/intellij/util/net/NetUtils.java
platform/platform-impl/src/com/intellij/ide/impl/ProjectUtil.java
platform/platform-impl/src/com/intellij/idea/SocketLock.java
platform/platform-impl/src/com/intellij/openapi/application/ConfigImportHelper.java
platform/platform-impl/src/com/intellij/openapi/vfs/impl/http/DefaultRemoteContentProvider.java
platform/platform-impl/src/com/intellij/util/Urls.java
platform/platform-impl/src/com/intellij/util/io/socketConnection/impl/SocketConnectionImpl.java
platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java [moved from platform/built-in-server-api/src/org/jetbrains/ide/HttpRequestHandler.java with 75% similarity]
platform/platform-impl/src/org/jetbrains/io/BuiltInServer.java
platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandler.kt
platform/platform-impl/src/org/jetbrains/io/DelegatingHttpRequestHandlerBase.kt
platform/platform-impl/src/org/jetbrains/io/FileResponses.kt
platform/platform-impl/src/org/jetbrains/io/NettyUtil.java
platform/platform-impl/src/org/jetbrains/io/PortUnificationServerHandler.java
platform/platform-impl/src/org/jetbrains/io/Responses.kt
platform/platform-impl/src/org/jetbrains/io/netty.kt
platform/platform-resources-en/src/messages/IdeBundle.properties
platform/platform-resources/src/META-INF/PlatformExtensionPoints.xml
platform/platform-resources/src/META-INF/built-in-server.xml
platform/platform-resources/src/META-INF/mime.types
platform/util/resources/misc/registry.properties
plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassApp.java
plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassXmlRpcClient.java
plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassXmlRpcHandler.java
plugins/git4idea/rt/src/org/jetbrains/git4idea/ssh/GitSSHHandler.java
plugins/git4idea/rt/src/org/jetbrains/git4idea/ssh/GitSSHXmlRpcClient.java
plugins/git4idea/rt/src/org/jetbrains/git4idea/ssh/SSHMain.java
plugins/git4idea/src/git4idea/commands/GitHandler.java
plugins/git4idea/src/git4idea/commands/GitHttpAuthService.java
plugins/git4idea/src/git4idea/rebase/GitRebaseEditorService.java
plugins/git4idea/src/org/jetbrains/git4idea/ssh/GitXmlRpcHandlerService.java
plugins/git4idea/src/org/jetbrains/git4idea/ssh/GitXmlRpcSshService.java
plugins/xpath/xpath-lang/src/org/intellij/lang/xpath/xslt/run/OutputTabAdapter.java
xml/impl/src/com/intellij/ide/browsers/BrowserLauncherImpl.java
xml/impl/src/com/intellij/ide/browsers/impl/WebBrowserServiceImpl.java
xml/impl/xml.iml
xml/openapi/src/com/intellij/ide/browsers/OpenInBrowserRequest.java

index 2d9742863629c584bcc2abb51d87bc3b622d012e..3983bf2b36ab814368c871a7fdb34c18a280c83b 100644 (file)
@@ -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();
   }
index 52596635976360b11dac4b4c2043512d9636942f..f09d7b5536397dd416424046c53fae5c4121d7c0 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright 2000-2011 JetBrains s.r.o.
+ * Copyright 2000-2016 JetBrains s.r.o.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,13 +17,13 @@ package com.intellij.execution.runners;
 
 import com.intellij.execution.process.ProcessHandler;
 import com.intellij.openapi.util.Key;
-import com.intellij.util.net.NetUtils;
 import org.jetbrains.annotations.NonNls;
 
 import java.io.BufferedWriter;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
+import java.net.InetAddress;
 import java.net.ServerSocket;
 import java.net.Socket;
 
@@ -95,7 +95,7 @@ class ProcessProxyImpl implements ProcessProxy {
     if (myWriter == null) {
       try {
         if (mySocket == null) {
-          mySocket = new Socket(NetUtils.getLoopbackAddress(), myPortNumber);
+          mySocket = new Socket(InetAddress.getLoopbackAddress(), myPortNumber);
         }
         myWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(mySocket.getOutputStream())));
       }
diff --git a/lib/annotations/netty/io/netty/resolver/annotations.xml b/lib/annotations/netty/io/netty/resolver/annotations.xml
new file mode 100644 (file)
index 0000000..1008967
--- /dev/null
@@ -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>
\ No newline at end of file
index fff0a1564531c3e0f495470b67b4f634de1a5459..448ede483363198895982a4a0c6d6f4ffae8b2e7 100644 (file)
@@ -18,8 +18,12 @@ package org.jetbrains.ide;
 import com.intellij.openapi.Disposable;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.components.ApplicationComponent;
+import com.intellij.util.Url;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import java.net.URLConnection;
+
 public abstract class BuiltInServerManager extends ApplicationComponent.Adapter {
   public static BuiltInServerManager getInstance() {
     return ApplicationManager.getApplication().getComponent(BuiltInServerManager.class);
@@ -31,4 +35,8 @@ public abstract class BuiltInServerManager extends ApplicationComponent.Adapter
 
   @Nullable
   public abstract Disposable getServerDisposable();
+  
+  public abstract boolean isOnBuiltInWebServer(@Nullable Url url);
+  
+  public abstract void configureRequestToWebServer(@NotNull URLConnection connection);
 }
\ No newline at end of file
index 1fa74d36be451589919320a42820f87e0e0e140a..471da19667f816050521eb5664245b2b581eb06a 100644 (file)
@@ -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()));
     }
   }
 }
index 1dd2f1b53e3c89aa9660738f2683f5863eda1838..513a36b4a38fb46498b53bda7a9bc02067ddcd24 100644 (file)
  */
 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.FileUtilRt
 import com.intellij.openapi.util.io.endsWithName
+import com.intellij.openapi.util.registry.Registry
+import com.intellij.openapi.util.text.StringUtil
 import com.intellij.openapi.vfs.VirtualFile
-import com.intellij.util.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 +103,72 @@ class BuiltInWebServer : HttpRequestHandler() {
     else {
       projectName = host
     }
-    return doProcess(request, context, projectName)
+    return doProcess(urlDecoder, request, context, projectName)
+  }
+}
+
+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(request: FullHttpRequest, context: ChannelHandlerContext, projectNameAsHost: String?): Boolean {
-  val decodedPath = URLUtil.unescapePercentSequences(UriUtil.trimParameters(request.uri()))
+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 +221,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 +248,59 @@ private fun doProcess(request: FullHttpRequest, context: ChannelHandlerContext,
   return false
 }
 
+internal fun HttpRequest.isSignedRequest(): Boolean {
+  // we must check referrer - if html cached, browser will send request without query
+  val token = headers().get(TOKEN_HEADER_NAME)
+      ?: QueryStringDecoder(uri()).parameters().get(TOKEN_PARAM_NAME)?.firstOrNull()
+      ?: referrer?.let { QueryStringDecoder(it).parameters().get(TOKEN_PARAM_NAME)?.firstOrNull() }
+
+  if (token != null && tokens.getIfPresent(token) != null) {
+    tokens.invalidate(token)
+    return true
+  }
+  else {
+    return false
+  }
+}
+
+@JvmOverloads
+internal fun validateToken(request: HttpRequest, channel: Channel, isSignedRequest: Boolean = request.isSignedRequest()): HttpHeaders? {
+  request.headers().get(HttpHeaderNames.COOKIE)?.let {
+    for (cookie in ServerCookieDecoder.STRICT.decode(it)) {
+      if (cookie.name() == STANDARD_COOKIE.name()) {
+        if (cookie.value() == STANDARD_COOKIE.value()) {
+          return EmptyHttpHeaders.INSTANCE
+        }
+        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 +370,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 +388,18 @@ 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
   }
+}
+
+internal fun canBeAccessedDirectly(path: String): Boolean {
+  for (fileHandler in WebServerFileHandler.EP_NAME.extensions) {
+    for (ext in fileHandler.pageFileExtensions) {
+      if (FileUtilRt.extensionEquals(path, ext)) {
+        return true
+      }
+    }
+  }
+  return false
 }
\ No newline at end of file
index 128e3be572dc06dc12d132b9a0dfccb817dccfce..2b2a53cdfb0aa2c36ed95f26df83b281ed3e7470 100644 (file)
@@ -20,13 +20,18 @@ import com.intellij.openapi.project.Project
 import com.intellij.openapi.util.io.endsWithName
 import com.intellij.openapi.util.io.endsWithSlash
 import com.intellij.openapi.util.io.getParentPath
+import com.intellij.openapi.vfs.VFileProperty
 import com.intellij.openapi.vfs.VirtualFile
 import com.intellij.util.PathUtilRt
+import com.intellij.util.isDirectory
+import io.netty.channel.Channel
 import io.netty.channel.ChannelHandlerContext
 import io.netty.handler.codec.http.FullHttpRequest
+import io.netty.handler.codec.http.HttpRequest
 import io.netty.handler.codec.http.HttpResponseStatus
-import org.jetbrains.io.send
+import org.jetbrains.io.*
 import java.nio.file.Path
+import java.nio.file.Paths
 
 private class DefaultWebServerPathHandler : WebServerPathHandler() {
   override fun process(path: String,
@@ -37,18 +42,17 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() {
                        decodedRawPath: String,
                        isCustomHost: Boolean): Boolean {
     val channel = context.channel()
+
+    val isSignedRequest = request.isSignedRequest()
+    val extraHttpHeaders = validateToken(request, channel, isSignedRequest) ?: return true
+
     val pathToFileManager = WebServerPathToFileManager.getInstance(project)
     var pathInfo = pathToFileManager.pathToInfoCache.getIfPresent(path)
     if (pathInfo == null || !pathInfo.isValid) {
       pathInfo = pathToFileManager.doFindByRelativePath(path)
       if (pathInfo == null) {
-        if (path.isEmpty()) {
-          HttpResponseStatus.NOT_FOUND.send(channel, request, "Index file doesn't exist.")
-          return true
-        }
-        else {
-          return false
-        }
+        HttpResponseStatus.NOT_FOUND.send(channel, request, if (path.isEmpty()) "Index file doesn't exist." else null, extraHttpHeaders)
+        return true
       }
 
       pathToFileManager.pathToInfoCache.put(path, pathInfo)
@@ -56,11 +60,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 +70,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 = extraHttpHeaders)
+        return true
+      }
+
+      // we must redirect only after index file check to not expose directory status
+      if (!endsWithSlash(decodedRawPath)) {
+        redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path", extraHttpHeaders)
         return true
       }
 
@@ -80,6 +85,12 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() {
       pathToFileManager.pathToInfoCache.put(path, pathInfo)
     }
 
+    // if extraHttpHeaders is not empty, it means that we get request wih token in the query
+    if (!isSignedRequest && request.origin == null && request.referrer == null && request.isRegularBrowser() && !canBeAccessedDirectly(pathInfo.name)) {
+      HttpResponseStatus.NOT_FOUND.send(channel, request)
+      return true
+    }
+
     if (!indexUsed && !endsWithName(path, pathInfo.name)) {
       if (endsWithSlash(decodedRawPath)) {
         indexUsed = true
@@ -88,20 +99,47 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() {
         // FallbackResource feature in action, /login requested, /index.php retrieved, we must not redirect /login to /login/
         val parentPath = getParentPath(pathInfo.path)
         if (parentPath != null && endsWithName(path, PathUtilRt.getFileName(parentPath))) {
-          redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path")
+          redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path", extraHttpHeaders)
           return true
         }
       }
     }
 
+    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, extraHttpHeaders)) {
           return true
         }
       }
     }
+
+    // we registered as a last handler, so, we should just return 404 and send extra headers
+    HttpResponseStatus.NOT_FOUND.send(channel, request, extraHeaders = extraHttpHeaders)
+    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 (!checkAccess(file, Paths.get(pathInfo.root.path))) {
+      HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request)
+      return false
+    }
+  }
+  else if (pathInfo.file!!.`is`(VFileProperty.HIDDEN)) {
+    HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request)
     return false
   }
+
+  return true
 }
\ No newline at end of file
index ea215e5357094db7ff4a657c71490c0a0c9564b4..457e39c19f5b079a9350ef2a71bebc0df86755be 100644 (file)
@@ -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"))
\ No newline at end of file
+// deny access to any dot prefixed file
+private fun hasAccess(result: Path) = Files.isReadable(result) && !(Files.isHidden(result) || result.fileName.toString().startsWith('.'))
\ No newline at end of file
index d2eed3717aca7bfb71b3c910fcbd1171082538e5..622322337912c45d75ffb8fbaa71cfcbd6e6936b 100644 (file)
@@ -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"
\ No newline at end of file
index 037a64cb5e8f35f1f33d1f76ce1b8919488bf66e..772c9c35a6a1228f4cdda5af6ed7542b76e73a03 100644 (file)
@@ -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)
 }
\ No newline at end of file
index 6e33b54cbe247232f6d157913060965170da9854..54cd4b34decbd25ba21072ac9905f92993bb8ede 100644 (file)
@@ -10,13 +10,22 @@ import com.intellij.openapi.application.ApplicationNamesInfo;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.util.Disposer;
 import com.intellij.openapi.util.NotNullLazyValue;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.util.Url;
+import com.intellij.util.net.NetUtils;
 import io.netty.channel.oio.OioEventLoopGroup;
 import org.jetbrains.annotations.NonNls;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
+import org.jetbrains.builtInWebServer.BuiltInServerOptions;
+import org.jetbrains.builtInWebServer.BuiltInWebServerKt;
 import org.jetbrains.io.BuiltInServer;
 import org.jetbrains.io.SubServer;
 
+import java.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 +125,16 @@ public class BuiltInServerManagerImpl extends BuiltInServerManager {
     return server;
   }
 
+  @Override
+  public boolean isOnBuiltInWebServer(@Nullable Url url) {
+    return url != null && !StringUtil.isEmpty(url.getAuthority()) && isOnBuiltInWebServerByAuthority(url.getAuthority());
+  }
+
+  @Override
+  public void configureRequestToWebServer(@NotNull URLConnection connection) {
+    connection.setRequestProperty(BuiltInWebServerKt.TOKEN_HEADER_NAME, BuiltInWebServerKt.acquireToken());
+  }
+
   private static void bindCustomPorts(@NotNull BuiltInServer server) {
     if (ApplicationManager.getApplication().isUnitTestMode()) {
       return;
@@ -130,4 +149,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;
+    }
+  }
 }
\ No newline at end of file
index d3b76cf3b5c19099d9fc121be433be075fa90699..061c32227831383f01c3f52e4f1453f096c54b7f 100644 (file)
@@ -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;
+  }
 }
diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java
deleted file mode 100644 (file)
index 1a6ddfc..0000000
+++ /dev/null
@@ -1,350 +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.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;
-              }
-              boolean value = com.intellij.ide.impl.ProjectUtil
-                .confirmLoadingFromRemotePath(systemIndependentName, "warning.load.file.from.share", "title.load.file.from.share");
-
-              if (value != Boolean.TRUE) {
-                lastTimeRejected = System.currentTimeMillis();
-                waitUntilNextRequestTimeout = Math.min(2 * Math.max(waitUntilNextRequestTimeout, 2000), 60 * 60 * 1000); //to avoid negative values
-                result.set(Promise.reject(NOT_FOUND));
-              } else {
-                waitUntilNextRequestTimeout = 0;
-              }
-            }
-          });
-        } catch (Throwable ignored) {}
-
-        if (result.get() != null) {
-          return result.get();
-        }
-      }
-
-      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;
-  }
-}
diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.kt
new file mode 100644 (file)
index 0000000..d924544
--- /dev/null
@@ -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
+}
diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java b/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java
deleted file mode 100644 (file)
index 9487bdc..0000000
+++ /dev/null
@@ -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;
-  }
-}
\ No newline at end of file
index 428635643535985174e3e90a59976c9ef469de37..da22bb803530f9885c6feb4e411b5f3879d76bf7 100644 (file)
@@ -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;
+  }
 }
index c0938646613f8aa7587809b6ae4197780f263611..877ac39cdfff7755520c50c7bd78c3e856db656e 100644 (file)
  */
 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,44 @@ public abstract class RestService extends HttpRequestHandler {
     return true;
   }
 
-  protected final void activateLastFocusedFrame() {
+  private boolean isHostTrusted(@NotNull FullHttpRequest request) throws InterruptedException, InvocationTargetException {
+    String referrer = NettyKt.getOrigin(request);
+    if (referrer == null) {
+      referrer = NettyKt.getReferrer(request);
+    }
+
+    String host;
+    try {
+      host = StringUtil.nullize(referrer == null ? null : new URI(referrer).getHost());
+    }
+    catch (URISyntaxException ignored) {
+      return false;
+    }
+
+    Ref<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 +262,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);
   }
 
index 3f26acc49bfd96d39f9f92ed49bceb42a04eb066..4ad9a213b92571d17fafe66006528d33392898e9 100644 (file)
  */
 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;
-        }
+    ByteBuf content = request.content();
+    if (content.readableBytes() == 0) {
+      Responses.send(HttpResponseStatus.BAD_REQUEST, context.channel(), request);
+      return true;
+    }
 
-        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();
+    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;
       }
 
-      Responses.send(Responses.response("text/xml", result), context.channel(), request);
+      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;
     }
-    return false;
+
+    Responses.send(Responses.response("text/xml", result), context.channel(), request);
+    return true;
   }
 
   private static Object getHandler(@NotNull String methodName, @NotNull Map<String, Object> handlers) {
index c5e58d805f36a4adc7b1cb42695483e018b7c061..67a52d832d21e1242ecbef37d5274e988d7c3b08 100644 (file)
@@ -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;
index 75d090042f7fc634b26810f75e9933afe4d1d63d..a295c1ca12fa4ee51d49fb37027223d25a4b3ab8 100644 (file)
@@ -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)
     }
   }
-}
\ No newline at end of file
+}
+
+private class ClientInfo(val channel: Channel, val extraHeaders: HttpHeaders)
\ No newline at end of file
index 5bed61f850813e82acdf42ea7b82ea468033d49b..4eb035b48410c6785f27c533d55ab691a0256959 100644 (file)
@@ -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
-      }
-
-      clients.forEach { client ->
-        if (client.channel.isActive) {
-          client.sendHeartbeat()
-        }
-        true
+  private val heartbeatTimer = SimpleTimer.getInstance().setUp({
+    forEachClient(TObjectProcedure {
+      if (it.channel.isActive) {
+        it.sendHeartbeat()
       }
-    }
+      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)
     }
   }
index 519cc7b4f440cd99be16aa66a00e1bf98318a659..d457fac331be20e1cbd19504360ba89ada04cb1a 100644 (file)
@@ -6,6 +6,7 @@ import com.intellij.openapi.module.EmptyModuleType
 import com.intellij.openapi.module.ModuleManager
 import com.intellij.openapi.project.Project
 import com.intellij.openapi.roots.ModuleRootModificationUtil
+import com.intellij.openapi.util.SystemInfo
 import com.intellij.openapi.util.io.FileUtil
 import com.intellij.openapi.util.text.StringUtil
 import com.intellij.openapi.vfs.LocalFileSystem
@@ -16,6 +17,7 @@ import org.assertj.core.api.Assertions.assertThat
 import org.junit.ClassRule
 import org.junit.Rule
 import org.junit.Test
+import java.nio.file.Files
 
 internal class BuiltInWebServerTest : BuiltInServerTestCase() {
   override val urlPathPrefix: String
@@ -89,4 +91,25 @@ internal class HeavyBuiltInWebServerTest {
       testUrl("http://localhost:${BuiltInServerManager.getInstance().port}/$webPath", HttpResponseStatus.NOT_FOUND)
     }
   }
+
+  @Test
+  fun `hidden dir`() {
+    val projectDir = tempDirManager.newPath().resolve("foo/bar")
+    val projectDirPath = projectDir.systemIndependentPath
+    createHeavyProject("$projectDirPath/test.ipr").use { project ->
+      projectDir.createDirectories()
+      LocalFileSystem.getInstance().refreshAndFindFileByPath(projectDirPath)
+      createModule(projectDirPath, project)
+
+      val dir = projectDir.resolve(".doNotExposeMe")
+      if (SystemInfo.isWindows) {
+        Files.setAttribute(dir, "dos:hidden", true)
+      }
+
+      val path = dir.resolve("foo").write("doNotExposeMe").systemIndependentPath
+      val relativePath = FileUtil.getRelativePath(project.basePath!!, path, '/')
+      val webPath = StringUtil.replace(UrlEscapers.urlPathSegmentEscaper().escape("${project.name}/$relativePath"), "%2F", "/")
+      testUrl("http://localhost:${BuiltInServerManager.getInstance().port}/$webPath", HttpResponseStatus.FORBIDDEN)
+    }
+  }
 }
\ No newline at end of file
diff --git a/platform/built-in-server/testSrc/IsLocalHostTest.kt b/platform/built-in-server/testSrc/IsLocalHostTest.kt
new file mode 100644 (file)
index 0000000..7221389
--- /dev/null
@@ -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()
+  }
+}
\ No newline at end of file
diff --git a/platform/platform-api/src/com/intellij/ide/XmlRpcHandlerBean.java b/platform/platform-api/src/com/intellij/ide/XmlRpcHandlerBean.java
deleted file mode 100644 (file)
index 77630f6..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright 2000-2015 JetBrains s.r.o.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.intellij.ide;
-
-import com.intellij.openapi.extensions.AbstractExtensionPointBean;
-import com.intellij.openapi.extensions.ExtensionPointName;
-import com.intellij.util.xmlb.annotations.Attribute;
-
-public class XmlRpcHandlerBean extends AbstractExtensionPointBean {
-  public static final ExtensionPointName<XmlRpcHandlerBean> EP_NAME = ExtensionPointName.create("com.intellij.xmlRpcHandler");
-
-  @Attribute("name")
-  public String name;
-
-  @Attribute("implementation")
-  public String implementation;
-}
\ No newline at end of file
index 16dafdcd8775ce9045a132268f60016cd0ed4606..eed4583c5ab01aacb54225a55d094b0af08d7d24 100644 (file)
@@ -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() {
index e6a2e956947e786193410f5c8604473ed0e1bd83..445027fec0269733ba3ba398e1864e341dba4748 100644 (file)
@@ -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() {
index e00978ee0c6a7d1132814325acda5c7b2c5811f9..f62c0f750d91ec02e52fbf175900d481e819b55e 100644 (file)
@@ -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);
   }
 }
index e2461d2b2d5987d934162e51e1b4c761d65fbad7..4340318b9f69a33866fbd6302a8a92e61a4b30a9 100644 (file)
@@ -209,15 +209,18 @@ public class ProjectUtil {
   public static boolean confirmLoadingFromRemotePath(@NotNull String path,
                                                      @NotNull @PropertyKey(resourceBundle = IdeBundle.BUNDLE) String msgKey,
                                                      @NotNull @PropertyKey(resourceBundle = IdeBundle.BUNDLE) String titleKey) {
+    return showYesNoDialog(IdeBundle.message(msgKey, path), titleKey);
+  }
+
+  public static boolean showYesNoDialog(@NotNull String message, @NotNull @PropertyKey(resourceBundle = IdeBundle.BUNDLE) String titleKey) {
     final Window window = getActiveFrameOrWelcomeScreen();
-    final String msg = IdeBundle.message(msgKey, path);
-    final String title = IdeBundle.message(titleKey);
     final Icon icon = Messages.getWarningIcon();
-    final int answer = window == null ? Messages.showYesNoDialog(msg, title, icon) : Messages.showYesNoDialog(window, msg, title, icon);
+    String title = IdeBundle.message(titleKey);
+    final int answer = window == null ? Messages.showYesNoDialog(message, title, icon) : Messages.showYesNoDialog(window, message, title, icon);
     return answer == Messages.YES;
   }
 
-  private static Window getActiveFrameOrWelcomeScreen() {
+  public static Window getActiveFrameOrWelcomeScreen() {
     Window window = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusedWindow();
     if (window != null)  return window;
 
index 0e4585b9f5f3f1f66e99458d28ada15ddb437c8c..0fcf78556d4fb1f75ac70c9a66280ef227417617 100644 (file)
@@ -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;
index 2329e369d9d90e35ab57ff48c229df01196b05fb..bbcd4bf3ebbc609a83fc36e0b44072e7177bbdf4 100644 (file)
@@ -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
index c74cecb41df77708a96f4737693309aa55fb0d76..c3b81522c35b1d936973aba2f1c98c700b932a3a 100644 (file)
@@ -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)
index 2ed214f6114f13aa4cb206d2ffa0b76ef2d74b35..5ffb69bb63c69b8310c988b062b6e39560be1b7d 100644 (file)
@@ -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);
index cfe3db1224a6badb9f604a0558efb7d01de2b490..5fc8fa284a29c75fb2d84153b4cdb7d9c1924b59 100644 (file)
@@ -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++) {
similarity index 75%
rename from platform/built-in-server-api/src/org/jetbrains/ide/HttpRequestHandler.java
rename to platform/platform-impl/src/org/jetbrains/ide/HttpRequestHandler.java
index 7ac77ba4955405000eb3a1e83bad59118e533b05..815264ada9a8e6f3129c8083ec00f86c82044401 100644 (file)
@@ -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;
-}
+}
\ No newline at end of file
index 35bc72e594ecbdef2994a8e5caed0597a15ff65c..7f5bf8f055bc8486abff44f2fee9191905648e98 100644 (file)
@@ -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;
index bfad9825627f2adfbf7ba15005f00de8560a30c6..806d3e6a351b790f5a4a7c7aeab652b779fc539b 100644 (file)
@@ -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") {
index bfe0014ae145f60aff02a5fa2d5e967efa96af04..b760cddf15617f62c12d09b9bcd90fb0575ef736 100644 (file)
@@ -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
index c00648bdb53d1fe251cea33999c5abb5fca0897d..6c083affd380f52da24c0b033ef10a4e6df3d4d1 100644 (file)
@@ -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)
 
index b3541f95b383c88601568be4e223ec1f734f8a37..33bd2693a253254d1787846f4681c853ae6c57a5 100644 (file)
@@ -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)
index 749487e22890cf28a4cfffc294f81e7896b27a0c..2ccd269c6fc8ca54a799c408474b54c8de010cbc 100644 (file)
@@ -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);
             }
index 3c293b37f0d4235f42701fd841cb323eecade772..3230f28b7d7bf1883f71482d610dd7c6fa6d9f53 100644 (file)
@@ -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()
index ceb922cc1a3444395c5f94fee4b79af21f409ee2..044302fdb57803d822326e9005784b30270f3bcc 100644 (file)
  */
 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)
 }
\ No newline at end of file
index f2c28e3b353128a5683b542511265ab1df8c25e0..ef95a23df1ed5817e4afe4d13fcbb463d303adc9 100644 (file)
@@ -1195,4 +1195,7 @@ edit.custom.settings.confirm=File \n''{0}''\n does not exist. Create?
 warning.load.project.from.share=You are opening a project from a network share. Do you trust this location?\n{0}
 title.load.project.from.share=Loading Project From Network
 warning.load.file.from.share=You are opening a file from a network share. Do you want to continue?\n{0}
-title.load.file.from.share=Loading File From Network
\ No newline at end of file
+title.load.file.from.share=Loading File From Network
+
+warning.use.rest.api=''{0}'' API is requested. Do you trust ''{1}''?
+title.use.rest.api=Using REST API
\ No newline at end of file
index 58990d478097f519f019d700ecd4023baad4c010..05e5bcead2250affbafcb91d0c2c4c91856a0396 100644 (file)
 
     <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>
     <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"/>
index b38e318efdefa5e079bec4bb2760c28f29653a11..cc795eaf1080d8a69808ea36247b39e67ea05973 100644 (file)
@@ -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"/>
index 3ae07a4849375c80f401754e44aa4ef2afd762bc..1dbfd66428f90b56e98bc5a0b9a86f15a42669c4 100644 (file)
   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
index 760c19f3e72208a558c3c894297f4b86eb1bf5e4..386102c34d2cd280982220d41d2222b234cbbecc 100644 (file)
@@ -730,4 +730,8 @@ ide.screenreader.autodetect.accessibility=false
 ide.screenreader.autodetect.accessibility.description=Automatically detect whether accessible context is enabled
 
 editor.rainbow.identifiers=false
-editor.rainbow.identifiers.description=Rainbow identifiers in editor
\ No newline at end of file
+editor.rainbow.identifiers.description=Rainbow identifiers in editor
+
+ide.http.server.response.actual.status=false
+ide.rest.api.requests.per.minute=30
+ide.built.in.web.server.activatable=false
\ No newline at end of file
index 8c8e425570a2f600f2fd467799d26c1e227f997f..8ce927804e52b8146282ea81d95761bdedc49339 100644 (file)
@@ -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
index 5d3f8681f0990e60e75aa3b87c595d3c8a2bf755..85e49697416ed35dbf805a66a819bd49d844ca51 100644 (file)
@@ -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 {
index 36cc365c1d80040b6c66c22fd1caea5d3fe6e632..9c9900a1f3caceb518231713c0d6b5429d5325cb 100644 (file)
@@ -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);
 
 }
index 2bc849d83fb0d94c42107a8b8d31983f1429cb26..4ce4ea31b6a25d87152f1ed7aa28e7ab1f028839 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright 2000-2012 JetBrains s.r.o.
+ * Copyright 2000-2016 JetBrains s.r.o.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -29,16 +29,10 @@ public interface GitSSHHandler {
    */
   @NonNls String GIT_SSH_PREFIX = "git-ssh-";
   /**
-   * Name of environment variable for SSH handler number
+   * Name of environment variable for SSH handler access token
    */
   @NonNls String SSH_HANDLER_ENV = "GIT4IDEA_SSH_HANDLER";
-  /**
-   * Name of environment variable for SSH handler number
-   */
   @NonNls String SSH_IGNORE_KNOWN_HOSTS_ENV = "GIT4IDEA_SSH_IGNORE_KNOWN_HOSTS";
-  /**
-   * Name of environment variable for SSH handler
-   */
   @NonNls String SSH_PORT_ENV = "GIT4IDEA_SSH_PORT";
   /**
    * Name of environment variable for SSH executable
@@ -60,7 +54,7 @@ public interface GitSSHHandler {
   /**
    * Verify server host key
    *
-   * @param handler                  a handler identifier
+   * @param token                    Access token.
    * @param hostName                 a host name
    * @param port                     a port number
    * @param serverHostKeyAlgorithm   an algorithm
@@ -68,7 +62,7 @@ public interface GitSSHHandler {
    * @param isNew                    true if the key is a new, false if the key was changed
    * @return true the host is verified, false otherwise
    */
-  boolean verifyServerHostKey(int handler,
+  boolean verifyServerHostKey(String token,
                               String hostName,
                               int port,
                               String serverHostKeyAlgorithm,
@@ -78,7 +72,7 @@ public interface GitSSHHandler {
   /**
    * Ask passphrase for the key
    *
-   * @param handler       a handler identifier
+   * @param token       Access token.
    * @param userName      a name of user
    * @param keyPath       a path for the key
    * @param resetPassword a reset password if one was stored in password database
@@ -86,12 +80,12 @@ public interface GitSSHHandler {
    * @return the passphrase entered by the user
    */
   @Nullable
-  String askPassphrase(final int handler, final String userName, final String keyPath, boolean resetPassword, final String lastError);
+  String askPassphrase(String token, final String userName, final String keyPath, boolean resetPassword, final String lastError);
 
   /**
    * Reply to challenge for keyboard-interactive method. Also used for
    *
-   * @param handlerNo   a handler identifier
+   * @param token   Access token.
    * @param userName    a user name (includes host and port)
    * @param name        name of challenge
    * @param instruction instruction
@@ -103,7 +97,7 @@ public interface GitSSHHandler {
    */
   @SuppressWarnings({"UseOfObsoleteCollectionType"})
   @Nullable
-  Vector<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);
 }
index c4ddd216e1cd08a67816981531dcffca2b5c6a3c..593362a3ee8eef3ccfa97d3f1d1d607130005a02 100644 (file)
@@ -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);
     }
   }
index b5bc260b95205032ebb61d238c54e188e0b806a5..67a74b92b0ac680fe8ec51b27c4af5859d235b00 100644 (file)
@@ -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;
index ef234be8808765ec7d585eee09dfb9dc2d15714c..52aec9f6388e50951f68a6a82a34c63e7143c579 100644 (file)
@@ -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;
index 6ddbcab22147b9f4ee7fc07e70cb842022fa8566..7cc1a8deb06cef22fef593ff5ca54195404198f6 100644 (file)
@@ -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);
     }
   }
 
index ea9d7ab8c2c07c5a5d7e575215b0d3cc5598ef4c..78874b69f6a0448892626a62fed94970fb830e6f 100644 (file)
@@ -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
    */
index 12c99d9d26ad6ff5da929837a40558e62ae15d3e..a07b2d63d235257e1871a917b608e796a07411fd 100644 (file)
@@ -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");
index 5d802d36e065965c91231b8734a40ef092d8528b..9b03e6db8c940cc0a100139ee54c13b88c310f95 100644 (file)
@@ -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);
     }
 
     /**
index e59ea477c6fd3ce3193d1fe19dd93df398608a5a..cb186d7025a1cc99e61ed7f81b71c4e7b221d601 100644 (file)
@@ -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);
 
index 65da7b30722cf8402df6a92cb0d8bbd233b83b99..4b509f7119a3b46f21f3c3f9ec97924784bbcb50 100644 (file)
@@ -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,36 @@ 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.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) {
+    if (Registry.is("ide.built.in.web.server.activatable", false) &&
+        BuiltInServerManager.getInstance().isOnBuiltInWebServer(Urls.parse(url, false))) {
+      PropertiesComponent.getInstance().setValue("ide.built.in.web.server.active", true);
+    }
+    
+    super.browse(url, browser, project);
+  }
+
   @Override
   protected void browseUsingNotSystemDefaultBrowserPolicy(@NotNull URI uri, @NotNull GeneralSettings settings, @Nullable Project project) {
     WebBrowserManager browserManager = WebBrowserManager.getInstance();
index be5151bb06a50825a579d340a9ffa7ba616a624e..9f23e779ed1931f33cc73d7dd92dc007ce81a828 100644 (file)
@@ -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();
index 2fb3ee7ea262d2abec2f412c586ac9247f0e5ace..447d4923d47e31d736b4a92d9e8155923ce8686b 100644 (file)
     <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>
\ No newline at end of file
index c4119d10c8c09079d728e91c1d175dfecf211a57..3f8d70e8522d059e6c14a92233133ac589aef115 100644 (file)
@@ -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;
+  }
 }
\ No newline at end of file