checkAccess in DefaultWebServerPathHandler — because each implementation of WebServer...
authorVladimir Krivosheev <vladimir.krivosheev@jetbrains.com>
Mon, 18 Apr 2016 10:39:16 +0000 (12:39 +0200)
committerVladimir Krivosheev <vladimir.krivosheev@jetbrains.com>
Fri, 22 Apr 2016 13:16:44 +0000 (15:16 +0200)
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/ide/OpenFileHttpService.kt
platform/platform-impl/src/org/jetbrains/io/Responses.kt

index 2e2c7395c70a0b6115438097004c9b1d9b1c0617..fae51c09cf8cbcf8f5abc0d55f1cc8f0e3834cd5 100644 (file)
@@ -138,7 +138,8 @@ private fun doProcess(request: FullHttpRequest, context: ChannelHandlerContext,
 
   val path = toIdeaPath(decodedPath, offset)
   if (path == null) {
-    HttpResponseStatus.BAD_REQUEST.send(context.channel(), request)
+    LOG.warn("$decodedPath is not valid")
+    HttpResponseStatus.NOT_FOUND.send(context.channel(), request)
     return true
   }
 
index 128e3be572dc06dc12d132b9a0dfccb817dccfce..a0bf723a00184cb860b31ad259c55ddd1035d821 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 java.nio.file.Path
+import java.nio.file.Paths
 
 private class DefaultWebServerPathHandler : WebServerPathHandler() {
   override fun process(path: String,
@@ -94,6 +99,10 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() {
       }
     }
 
+    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 {
@@ -104,4 +113,23 @@ private class DefaultWebServerPathHandler : WebServerPathHandler() {
     }
     return false
   }
+}
+
+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.NOT_FOUND.send(channel, request)
+      return false
+    }
+    else if (!checkAccess(channel, file, request, Paths.get(pathInfo.root.path))) {
+      return false
+    }
+  }
+  else if (pathInfo.file!!.`is`(VFileProperty.HIDDEN)) {
+    HttpResponseStatus.NOT_FOUND.send(channel, request)
+    return false
+  }
+
+  return true
 }
\ No newline at end of file
index 2051af71edd425bb8ca801693a0745245cffc143..d1b67c2f6cd9f6283990c6b922a470257276d274 100644 (file)
@@ -1,9 +1,7 @@
 package org.jetbrains.builtInWebServer
 
 import com.intellij.openapi.project.Project
-import com.intellij.openapi.vfs.VFileProperty
 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,6 +11,7 @@ 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.okInSafeMode
 import org.jetbrains.io.send
 import java.nio.file.Files
 import java.nio.file.Path
@@ -31,15 +30,10 @@ private class StaticFileHandler : WebServerFileHandler() {
         return true
       }
 
-      sendIoFile(channel, ioFile, Paths.get(pathInfo.root.path), request)
+      FileResponses.sendFile(request, channel, ioFile)
     }
     else {
       val file = pathInfo.file!!
-      if (file.`is`(VFileProperty.HIDDEN)) {
-        HttpResponseStatus.FORBIDDEN.send(channel, request)
-        return true
-      }
-
       val response = FileResponses.prepareSend(request, channel, file.timeStamp, file.name) ?: return true
 
       val keepAlive = response.addKeepAliveIfNeed(request)
@@ -98,20 +92,11 @@ private class StaticFileHandler : WebServerFileHandler() {
   }
 }
 
-private fun sendIoFile(channel: Channel, file: Path, root: Path, request: HttpRequest) {
-  if (file.isDirectory()) {
-    HttpResponseStatus.FORBIDDEN.send(channel, request)
-  }
-  else if (checkAccess(channel, file, request, root)) {
-    FileResponses.sendFile(request, channel, file)
-  }
-}
-
-fun checkAccess(channel: Channel, file: Path, request: HttpRequest, root: Path = file.root, doNotExposeStatus: Boolean = false): Boolean {
+internal fun checkAccess(channel: Channel, file: Path, request: HttpRequest, root: Path = file.root): Boolean {
   var parent = file
   do {
     if (!hasAccess(parent)) {
-      (if (doNotExposeStatus) HttpResponseStatus.OK else HttpResponseStatus.FORBIDDEN).send(channel, request)
+      HttpResponseStatus.FORBIDDEN.okInSafeMode().send(channel, request)
       return false
     }
     parent = parent.parent ?: break
@@ -120,5 +105,5 @@ fun checkAccess(channel: Channel, file: Path, request: HttpRequest, root: Path =
   return true
 }
 
-// deny access to .htaccess files
-private fun hasAccess(result: Path) = 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 8417d3dbcddb1b4a3a8395b4472a5bd8a480b7d6..96ca5601edbb12508c27578d8ee6c9a41627c394 100644 (file)
@@ -42,6 +42,7 @@ import org.jetbrains.concurrency.AsyncPromise
 import org.jetbrains.concurrency.Promise
 import org.jetbrains.concurrency.catchError
 import org.jetbrains.concurrency.rejectedPromise
+import org.jetbrains.io.okInSafeMode
 import java.nio.file.Path
 import java.nio.file.Paths
 import java.util.concurrent.ConcurrentLinkedQueue
@@ -122,7 +123,7 @@ internal class OpenFileHttpService : RestService() {
       .rejected {
         if (it === NOT_FOUND) {
           // don't expose file status
-          sendStatus(HttpResponseStatus.OK, keepAlive, channel)
+          sendStatus(HttpResponseStatus.NOT_FOUND.okInSafeMode(), keepAlive, channel)
           LOG.warn("File ${apiRequest.file} not found")
         }
         else {
@@ -141,7 +142,7 @@ internal class OpenFileHttpService : RestService() {
       if (!file.exists()) {
         return rejectedPromise(NOT_FOUND)
       }
-      return if (context == null || checkAccess(context.channel(), file, httpRequest!!, doNotExposeStatus = true)) openAbsolutePath(file, request) else null
+      return if (context == null || checkAccess(context.channel(), file, httpRequest!!)) openAbsolutePath(file, request) else null
     }
 
     // we don't want to call refresh for each attempt on findFileByRelativePath call, so, we do what ourSaveAndSyncHandlerImpl does on frame activation
index 3efbd25970829cd53e5e0f30f15d0f2b157cab10..4a3bd36e3523bb3f122db009e2a95d574ca75822 100644 (file)
@@ -103,6 +103,7 @@ fun HttpResponse.addCommonHeaders() {
     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) {
@@ -125,6 +126,8 @@ fun HttpResponseStatus.send(channel: Channel, request: HttpRequest? = null, desc
   createStatusResponse(this, request, description).send(channel, request)
 }
 
+fun HttpResponseStatus.okInSafeMode() = if (ApplicationManager.getApplication()?.isUnitTestMode ?: false) this else HttpResponseStatus.OK
+
 private fun createStatusResponse(responseStatus: HttpResponseStatus, request: HttpRequest?, description: String?): HttpResponse {
   if (request != null && request.method() === HttpMethod.HEAD) {
     return responseStatus.response()