2 * Copyright 2000-2014 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package org.jetbrains.builtInWebServer
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
39 private val chromeVersionFromUserAgent = Pattern.compile(" Chrome/([\\d.]+) ")
41 private class DefaultWebServerPathHandler : WebServerPathHandler() {
42 override fun process(path: String,
44 request: FullHttpRequest,
45 context: ChannelHandlerContext,
47 decodedRawPath: String,
48 isCustomHost: Boolean): Boolean {
49 val channel = context.channel()
51 val isSignedRequest = request.isSignedRequest()
52 val extraHeaders = validateToken(request, channel, isSignedRequest) ?: return true
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)
63 pathToFileManager.pathToInfoCache.put(path, pathInfo)
67 if (pathInfo.isDirectory()) {
68 var indexVirtualFile: VirtualFile? = null
69 var indexFile: Path? = null
70 if (pathInfo.file == null) {
71 indexFile = findIndexFile(pathInfo.ioFile!!)
74 indexVirtualFile = findIndexFile(pathInfo.file!!)
77 if (indexFile == null && indexVirtualFile == null) {
78 HttpResponseStatus.NOT_FOUND.send(channel, request, extraHeaders = extraHeaders)
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)
89 pathInfo = PathInfo(indexFile, indexVirtualFile, pathInfo.root, pathInfo.moduleName, pathInfo.isLibrary)
90 pathToFileManager.pathToInfoCache.put(path, pathInfo)
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)
102 if (!indexUsed && !endsWithName(path, pathInfo.name)) {
103 if (endsWithSlash(decodedRawPath)) {
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)
116 if (!checkAccess(pathInfo, channel, request)) {
120 val canonicalPath = if (indexUsed) "$path/${pathInfo.name}" else path
121 for (fileHandler in WebServerFileHandler.EP_NAME.extensions) {
123 if (fileHandler.process(pathInfo!!, canonicalPath, project, request, channel, if (isCustomHost) null else projectName, extraHeaders)) {
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)
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)
142 else if (!checkAccess(file, Paths.get(pathInfo.root.path))) {
143 HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request)
147 else if (pathInfo.file!!.`is`(VFileProperty.HIDDEN)) {
148 HttpResponseStatus.FORBIDDEN.orInSafeMode(HttpResponseStatus.NOT_FOUND).send(channel, request)
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)) {