2876c923e9bd9f2e5c0eda5309006d7b71f846bc
[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 (!checkAccess(file, Paths.get(pathInfo.root.path))) {
143       HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request)
144       return false
145     }
146   }
147   else if (pathInfo.file!!.`is`(VFileProperty.HIDDEN)) {
148     HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request)
149     return false
150   }
151
152   return true
153 }
154
155 private fun canBeAccessedDirectly(path: String): Boolean {
156   for (fileHandler in WebServerFileHandler.EP_NAME.extensions) {
157     for (ext in fileHandler.pageFileExtensions) {
158       if (FileUtilRt.extensionEquals(path, ext)) {
159         return true
160       }
161     }
162   }
163   return false
164 }