WEB-21594 Web preview opens a 404 Not Found document
[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.FileUtilRt
21 import com.intellij.openapi.util.io.endsWithName
22 import com.intellij.openapi.util.io.endsWithSlash
23 import com.intellij.openapi.util.io.getParentPath
24 import com.intellij.openapi.util.text.StringUtil
25 import com.intellij.openapi.vfs.VFileProperty
26 import com.intellij.openapi.vfs.VirtualFile
27 import com.intellij.util.PathUtilRt
28 import com.intellij.util.isDirectory
29 import io.netty.channel.Channel
30 import io.netty.channel.ChannelHandlerContext
31 import io.netty.handler.codec.http.FullHttpRequest
32 import io.netty.handler.codec.http.HttpRequest
33 import io.netty.handler.codec.http.HttpResponseStatus
34 import org.jetbrains.io.*
35 import java.nio.file.Path
36 import java.nio.file.Paths
37 import java.util.regex.Pattern
38
39 private val chromeVersionFromUserAgent = Pattern.compile(" Chrome/([\\d.]+) ")
40
41 private class DefaultWebServerPathHandler : WebServerPathHandler() {
42   override fun process(path: String,
43                        project: Project,
44                        request: FullHttpRequest,
45                        context: ChannelHandlerContext,
46                        projectName: String,
47                        decodedRawPath: String,
48                        isCustomHost: Boolean): Boolean {
49     val channel = context.channel()
50
51     val isSignedRequest = request.isSignedRequest()
52     val extraHeaders = validateToken(request, channel, isSignedRequest) ?: return true
53
54     val pathToFileManager = WebServerPathToFileManager.getInstance(project)
55     var pathInfo = pathToFileManager.pathToInfoCache.getIfPresent(path)
56     if (pathInfo == null || !pathInfo.isValid) {
57       pathInfo = pathToFileManager.doFindByRelativePath(path)
58       if (pathInfo == null) {
59         HttpResponseStatus.NOT_FOUND.send(channel, request, extraHeaders = extraHeaders)
60         return true
61       }
62
63       pathToFileManager.pathToInfoCache.put(path, pathInfo)
64     }
65
66     var indexUsed = false
67     if (pathInfo.isDirectory()) {
68       var indexVirtualFile: VirtualFile? = null
69       var indexFile: Path? = null
70       if (pathInfo.file == null) {
71         indexFile = findIndexFile(pathInfo.ioFile!!)
72       }
73       else {
74         indexVirtualFile = findIndexFile(pathInfo.file!!)
75       }
76
77       if (indexFile == null && indexVirtualFile == null) {
78         HttpResponseStatus.NOT_FOUND.send(channel, request, extraHeaders = extraHeaders)
79         return true
80       }
81
82       // we must redirect only after index file check to not expose directory status
83       if (!endsWithSlash(decodedRawPath)) {
84         redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path", extraHeaders)
85         return true
86       }
87
88       indexUsed = true
89       pathInfo = PathInfo(indexFile, indexVirtualFile, pathInfo.root, pathInfo.moduleName, pathInfo.isLibrary)
90       pathToFileManager.pathToInfoCache.put(path, pathInfo)
91     }
92
93     val userAgent = request.userAgent
94     if (!isSignedRequest && userAgent != null && request.isRegularBrowser() && request.origin == null && request.referrer == null) {
95       val matcher = chromeVersionFromUserAgent.matcher(userAgent)
96       if (matcher.find() && StringUtil.compareVersionNumbers(matcher.group(1), "51") < 0 && !canBeAccessedDirectly(pathInfo.name)) {
97         HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request)
98         return true
99       }
100     }
101
102     if (!indexUsed && !endsWithName(path, pathInfo.name)) {
103       if (endsWithSlash(decodedRawPath)) {
104         indexUsed = true
105       }
106       else {
107         // FallbackResource feature in action, /login requested, /index.php retrieved, we must not redirect /login to /login/
108         val parentPath = getParentPath(pathInfo.path)
109         if (parentPath != null && endsWithName(path, PathUtilRt.getFileName(parentPath))) {
110           redirectToDirectory(request, channel, if (isCustomHost) path else "$projectName/$path", extraHeaders)
111           return true
112         }
113       }
114     }
115
116     if (!checkAccess(pathInfo, channel, request)) {
117       return true
118     }
119
120     val canonicalPath = if (indexUsed) "$path/${pathInfo.name}" else path
121     for (fileHandler in WebServerFileHandler.EP_NAME.extensions) {
122       LOG.catchAndLog {
123         if (fileHandler.process(pathInfo!!, canonicalPath, project, request, channel, if (isCustomHost) null else projectName, extraHeaders)) {
124           return true
125         }
126       }
127     }
128
129     // we registered as a last handler, so, we should just return 404 and send extra headers
130     HttpResponseStatus.NOT_FOUND.send(channel, request, extraHeaders = extraHeaders)
131     return true
132   }
133 }
134
135 private fun checkAccess(pathInfo: PathInfo, channel: Channel, request: HttpRequest): Boolean {
136   if (pathInfo.ioFile != null || pathInfo.file!!.isInLocalFileSystem) {
137     val file = pathInfo.ioFile ?: Paths.get(pathInfo.file!!.path)
138     if (file.isDirectory()) {
139       HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request)
140       return false
141     }
142     else if (!hasAccess(file)) {
143       // we check only file, but all directories in the path because of https://youtrack.jetbrains.com/issue/WEB-21594
144       HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request)
145       return false
146     }
147   }
148   else if (pathInfo.file!!.`is`(VFileProperty.HIDDEN)) {
149     HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request)
150     return false
151   }
152
153   return true
154 }
155
156 private fun canBeAccessedDirectly(path: String): Boolean {
157   for (fileHandler in WebServerFileHandler.EP_NAME.extensions) {
158     for (ext in fileHandler.pageFileExtensions) {
159       if (FileUtilRt.extensionEquals(path, ext)) {
160         return true
161       }
162     }
163   }
164   return false
165 }