rest api: limit number of requests per minite and check is host trusted
authorVladimir Krivosheev <vladimir.krivosheev@jetbrains.com>
Thu, 21 Apr 2016 11:49:07 +0000 (13:49 +0200)
committerVladimir Krivosheev <vladimir.krivosheev@jetbrains.com>
Fri, 22 Apr 2016 13:17:33 +0000 (15:17 +0200)
platform/built-in-server/src/org/jetbrains/ide/RestService.java
platform/platform-impl/src/com/intellij/ide/impl/ProjectUtil.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/util/resources/misc/registry.properties

index 3ea42c11956a40541e2335e282aa0c621b7d4995..e29d40851114b2bb846004046995b08b6c50000b 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 {
+      if (!isHostTrusted(request)) {
+        Responses.send(Responses.orInSafeMode(HttpResponseStatus.FORBIDDEN, HttpResponseStatus.OK), context.channel(), request);
+        return true;
+      }
+
+      AtomicInteger counter = abuseCounter.get(((InetSocketAddress)context.channel().remoteAddress()).getAddress());
+      if (counter.incrementAndGet() > Registry.intValue("ide.rest.api.requests.per.minute", 60)) {
+        Responses.send(Responses.orInSafeMode(HttpResponseStatus.TOO_MANY_REQUESTS, HttpResponseStatus.OK), context.channel(), request);
+        return true;
+      }
+
       String error = execute(urlDecoder, request, context);
       if (error != null) {
         Responses.send(HttpResponseStatus.BAD_REQUEST, context.channel(), request, error);
@@ -137,7 +175,44 @@ public abstract class RestService extends HttpRequestHandler {
     return true;
   }
 
-  protected final void activateLastFocusedFrame() {
+  private boolean isHostTrusted(@NotNull FullHttpRequest request) throws InterruptedException, InvocationTargetException {
+    String referrer = NettyKt.getOrigin(request);
+    if (referrer == null) {
+      referrer = NettyKt.getReferrer(request);
+    }
+
+    String host;
+    try {
+      host = StringUtil.nullize(referrer == null ? null : new URI(referrer).getHost());
+    }
+    catch (URISyntaxException ignored) {
+      return false;
+    }
+
+    Ref<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();
index e2461d2b2d5987d934162e51e1b4c761d65fbad7..2a58f4691e267040417f5d8b8a4ac56bbdc91baf 100644 (file)
@@ -209,11 +209,14 @@ public class ProjectUtil {
   public static boolean confirmLoadingFromRemotePath(@NotNull String path,
                                                      @NotNull @PropertyKey(resourceBundle = IdeBundle.BUNDLE) String msgKey,
                                                      @NotNull @PropertyKey(resourceBundle = IdeBundle.BUNDLE) String titleKey) {
+    return showYesNoDialog(IdeBundle.message(msgKey, path), titleKey);
+  }
+
+  public static boolean showYesNoDialog(@NotNull String message, @NotNull @PropertyKey(resourceBundle = IdeBundle.BUNDLE) String titleKey) {
     final Window window = getActiveFrameOrWelcomeScreen();
-    final String msg = IdeBundle.message(msgKey, path);
-    final String title = IdeBundle.message(titleKey);
     final Icon icon = Messages.getWarningIcon();
-    final int answer = window == null ? Messages.showYesNoDialog(msg, title, icon) : Messages.showYesNoDialog(window, msg, title, icon);
+    String title = IdeBundle.message(titleKey);
+    final int answer = window == null ? Messages.showYesNoDialog(message, title, icon) : Messages.showYesNoDialog(window, message, title, icon);
     return answer == Messages.YES;
   }
 
index b21cfa40ffd14e9f18f941d599c13529b8a0c12d..533cdea6c2cc8a1977c9a43f05110e3b50215d4a 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
@@ -126,7 +127,14 @@ fun HttpResponseStatus.send(channel: Channel, request: HttpRequest? = null, desc
   createStatusResponse(this, request, description).send(channel, request)
 }
 
-fun HttpResponseStatus.orInSafeMode(safeStatus: HttpResponseStatus) = if (ApplicationManager.getApplication()?.isUnitTestMode ?: false) this else safeStatus
+fun HttpResponseStatus.orInSafeMode(safeStatus: HttpResponseStatus): HttpResponseStatus {
+  if (!Registry.`is`("ide.rest.api.paranoid.mode", true) || (ApplicationManager.getApplication()?.isUnitTestMode ?: false)) {
+    return this
+  }
+  else {
+    return safeStatus
+  }
+}
 
 private fun createStatusResponse(responseStatus: HttpResponseStatus, request: HttpRequest?, description: String?): HttpResponse {
   if (request != null && request.method() === HttpMethod.HEAD) {
index fd09296971cd6ebdccd2dbb0d34b082869a66586..0d0c87f8d0901bb1e46f75d1f21ee3e603eee6aa 100644 (file)
@@ -187,7 +187,6 @@ fun HttpRequest.isRegularBrowser() = userAgent?.startsWith("Mozilla/5.0") ?: fal
 
 // forbid POST requests from browser without Origin
 fun HttpRequest.isWriteFromBrowserWithoutOrigin(): Boolean {
-  val userAgent = userAgent ?: return false
   val method = method()
   return origin.isNullOrEmpty() && isRegularBrowser() && (method == HttpMethod.POST || method == HttpMethod.PATCH || method == HttpMethod.PUT || method == HttpMethod.DELETE)
 }
\ No newline at end of file
index 88607e0eaf70f96d6480c8f242245207a414da92..824424dda7aeb192ec4810554ce4a7666ec2cab3 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 a3d6810581ba2df2af5d2ab5d46b3527ecf7ecd9..a80847d6c2eb0b3b26a12430e3d32d497fb2d92c 100644 (file)
@@ -731,4 +731,7 @@ ide.screenreader.autodetect.accessibility=false
 ide.screenreader.autodetect.accessibility.description=Automatically detect whether accessible context is enabled
 
 editor.rainbow.identifiers=false
-editor.rainbow.identifiers.description=Rainbow identifiers in editor
\ No newline at end of file
+editor.rainbow.identifiers.description=Rainbow identifiers in editor
+
+ide.rest.api.paranoid.mode=true
+ide.rest.api.requests.per.minute=60
\ No newline at end of file