IDEA-155360 Open In Browser : files from library jars cannot be opened
[idea/community.git] / platform / built-in-server / src / org / jetbrains / builtInWebServer / DefaultWebServerPathHandler.kt
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.jetbrains.builtInWebServer
17
18 import com.intellij.openapi.diagnostic.catchAndLog
19 import com.intellij.openapi.project.Project
20 import com.intellij.openapi.util.io.endsWithName
21 import com.intellij.openapi.util.io.endsWithSlash
22 import com.intellij.openapi.util.io.getParentPath
23 import com.intellij.openapi.vfs.VFileProperty
24 import com.intellij.openapi.vfs.VirtualFile
25 import com.intellij.util.PathUtilRt
26 import com.intellij.util.isDirectory
27 import io.netty.channel.Channel
28 import io.netty.channel.ChannelHandlerContext
29 import io.netty.handler.codec.http.FullHttpRequest
30 import io.netty.handler.codec.http.HttpRequest
31 import io.netty.handler.codec.http.HttpResponseStatus
32 import org.jetbrains.io.*
33 import java.nio.file.Path
34 import java.nio.file.Paths
35
36 private class DefaultWebServerPathHandler : WebServerPathHandler() {
37   override fun process(path: String,
38                        project: Project,
39                        request: FullHttpRequest,
40                        context: ChannelHandlerContext,
41                        projectName: String,
42                        decodedRawPath: String,
43                        isCustomHost: Boolean): Boolean {
44     val isSignedRequest = request.isSignedRequest()
45     val extraHttpHeaders = validateToken(request, context.channel(), isSignedRequest) ?: return true
46
47     val channel = context.channel()
48     val pathToFileManager = WebServerPathToFileManager.getInstance(project)
49     var pathInfo = pathToFileManager.pathToInfoCache.getIfPresent(path)
50     if (pathInfo == null || !pathInfo.isValid) {
51       pathInfo = pathToFileManager.doFindByRelativePath(path)
52       if (pathInfo == null) {
53         if (path.isEmpty()) {
54           HttpResponseStatus.NOT_FOUND.send(channel, request, "Index file doesn't exist.", extraHttpHeaders)
55           return true
56         }
57         else {
58           return false
59         }
60       }
61
62       pathToFileManager.pathToInfoCache.put(path, pathInfo)
63     }
64
65     var indexUsed = false
66     if (pathInfo.isDirectory()) {
67       var indexVirtualFile: VirtualFile? = null
68       var indexFile: Path? = null
69       if (pathInfo.file == null) {
70         indexFile = findIndexFile(pathInfo.ioFile!!)
71       }
72       else {
73         indexVirtualFile = findIndexFile(pathInfo.file!!)
74       }
75
76       if (indexFile == null && indexVirtualFile == null) {
77         HttpResponseStatus.NOT_FOUND.send(channel, request, extraHeaders = extraHttpHeaders)
78         return true
79       }
80
81       // we must redirect only after index file check to not expose directory status
82       if (!endsWithSlash(decodedRawPath)) {
83         redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path", extraHttpHeaders)
84         return true
85       }
86
87       indexUsed = true
88       pathInfo = PathInfo(indexFile, indexVirtualFile, pathInfo.root, pathInfo.moduleName, pathInfo.isLibrary)
89       pathToFileManager.pathToInfoCache.put(path, pathInfo)
90     }
91
92     // if extraHttpHeaders is not empty, it means that we get request wih token in the query
93     if (!isSignedRequest && request.origin == null && request.referrer == null && request.isRegularBrowser() && !canBeAccessedDirectly(pathInfo.name)) {
94       HttpResponseStatus.NOT_FOUND.send(context.channel(), request)
95       return true
96     }
97
98     if (!indexUsed && !endsWithName(path, pathInfo.name)) {
99       if (endsWithSlash(decodedRawPath)) {
100         indexUsed = true
101       }
102       else {
103         // FallbackResource feature in action, /login requested, /index.php retrieved, we must not redirect /login to /login/
104         val parentPath = getParentPath(pathInfo.path)
105         if (parentPath != null && endsWithName(path, PathUtilRt.getFileName(parentPath))) {
106           redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path", extraHttpHeaders)
107           return true
108         }
109       }
110     }
111
112     if (!checkAccess(pathInfo, channel, request)) {
113       return true
114     }
115
116     val canonicalPath = if (indexUsed) "$path/${pathInfo.name}" else path
117     for (fileHandler in WebServerFileHandler.EP_NAME.extensions) {
118       LOG.catchAndLog {
119         if (fileHandler.process(pathInfo!!, canonicalPath, project, request, channel, if (isCustomHost) null else projectName, extraHttpHeaders)) {
120           return true
121         }
122       }
123     }
124     return false
125   }
126 }
127
128 private fun checkAccess(pathInfo: PathInfo, channel: Channel, request: HttpRequest): Boolean {
129   if (pathInfo.ioFile != null || pathInfo.file!!.isInLocalFileSystem) {
130     val file = pathInfo.ioFile ?: Paths.get(pathInfo.file!!.path)
131     if (file.isDirectory()) {
132       HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request)
133       return false
134     }
135     else if (!checkAccess(file, Paths.get(pathInfo.root.path))) {
136       HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request)
137       return false
138     }
139   }
140   else if (pathInfo.file!!.`is`(VFileProperty.HIDDEN)) {
141     HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request)
142     return false
143   }
144
145   return true
146 }