diff options
Diffstat (limited to 'xml/impl/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java')
-rw-r--r-- | xml/impl/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java | 231 |
1 files changed, 231 insertions, 0 deletions
diff --git a/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java b/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java new file mode 100644 index 000000000000..2dc24ba01706 --- /dev/null +++ b/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java @@ -0,0 +1,231 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.builtInWebServer; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; +import com.intellij.openapi.util.SystemInfoRt; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.UriUtil; +import com.intellij.util.io.URLUtil; +import com.intellij.util.net.NetUtils; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.ide.HttpRequestHandler; +import org.jetbrains.io.FileResponses; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import static org.jetbrains.io.Responses.sendOptionsResponse; +import static org.jetbrains.io.Responses.sendStatus; + +public final class BuiltInWebServer extends HttpRequestHandler { + static final Logger LOG = Logger.getInstance(BuiltInWebServer.class); + + @Nullable + public static VirtualFile findIndexFile(@NotNull VirtualFile basedir) { + VirtualFile[] children = basedir.getChildren(); + if (children == null || children.length == 0) { + return null; + } + + for (String indexNamePrefix : new String[]{"index.", "default."}) { + VirtualFile index = null; + String preferredName = indexNamePrefix + "html"; + for (VirtualFile child : children) { + if (!child.isDirectory()) { + String name = child.getName(); + if (name.equals(preferredName)) { + return child; + } + else if (index == null && name.startsWith(indexNamePrefix)) { + index = child; + } + } + } + if (index != null) { + return index; + } + } + return null; + } + + @Override + public boolean isSupported(@NotNull FullHttpRequest request) { + return super.isSupported(request) || request.method() == HttpMethod.POST || request.method() == HttpMethod.OPTIONS; + } + + @Override + public boolean process(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) { + if (request.method() == HttpMethod.OPTIONS) { + sendOptionsResponse("GET, POST, HEAD, OPTIONS", request, context); + return true; + } + + String host = HttpHeaders.getHost(request); + if (StringUtil.isEmpty(host)) { + return false; + } + + int portIndex = host.indexOf(':'); + if (portIndex > 0) { + host = host.substring(0, portIndex); + } + + String projectName; + boolean isIpv6 = host.charAt(0) == '[' && host.length() > 2 && host.charAt(host.length() - 1) == ']'; + if (isIpv6) { + host = host.substring(1, host.length() - 1); + } + + if (isIpv6 || Character.digit(host.charAt(0), 10) != -1 || host.charAt(0) == ':' || isOwnHostName(host)) { + if (urlDecoder.path().length() < 2) { + return false; + } + projectName = null; + } + else { + projectName = host; + } + return doProcess(request, context.channel(), projectName); + } + + public static boolean isOwnHostName(@NotNull String host) { + if (NetUtils.isLocalhost(host)) { + return true; + } + + try { + InetAddress address = InetAddress.getByName(host); + if (host.equals(address.getHostAddress()) || host.equalsIgnoreCase(address.getCanonicalHostName())) { + return true; + } + + String localHostName = InetAddress.getLocalHost().getHostName(); + // WEB-8889 + // develar.local is own host name: develar. equals to "develar.labs.intellij.net" (canonical host name) + return localHostName.equalsIgnoreCase(host) || + (host.endsWith(".local") && localHostName.regionMatches(true, 0, host, 0, host.length() - ".local".length())); + } + catch (UnknownHostException ignored) { + return false; + } + } + + private static boolean doProcess(@NotNull FullHttpRequest request, @NotNull Channel channel, @Nullable String projectName) { + final String decodedPath = URLUtil.unescapePercentSequences(UriUtil.trimParameters(request.uri())); + int offset; + boolean emptyPath; + boolean isCustomHost = projectName != null; + if (isCustomHost) { + // host mapped to us + offset = 0; + emptyPath = decodedPath.isEmpty(); + } + else { + offset = decodedPath.indexOf('/', 1); + projectName = decodedPath.substring(1, offset == -1 ? decodedPath.length() : offset); + emptyPath = offset == -1; + } + + Project project = findProject(projectName, isCustomHost); + if (project == null) { + return false; + } + + if (emptyPath) { + if (!SystemInfoRt.isFileSystemCaseSensitive) { + // may be passed path is not correct + projectName = project.getName(); + } + + // we must redirect "jsdebug" to "jsdebug/" as nginx does, otherwise browser will treat it as file instead of directory, so, relative path will not work + WebServerPathHandler.redirectToDirectory(request, channel, projectName); + return true; + } + + final String path = FileUtil.toCanonicalPath(decodedPath.substring(offset + 1), '/'); + LOG.assertTrue(path != null); + + for (WebServerPathHandler pathHandler : WebServerPathHandler.EP_NAME.getExtensions()) { + try { + if (pathHandler.process(path, project, request, channel, projectName, decodedPath, isCustomHost)) { + return true; + } + } + catch (Throwable e) { + LOG.error(e); + } + } + return false; + } + + static final class StaticFileHandler extends WebServerFileHandler { + @Override + public boolean process(@NotNull VirtualFile file, + @NotNull CharSequence canonicalRequestPath, + @NotNull Project project, + @NotNull FullHttpRequest request, + @NotNull Channel channel) throws IOException { + File ioFile = VfsUtilCore.virtualToIoFile(file); + if (hasAccess(ioFile)) { + FileResponses.sendFile(request, channel, ioFile); + } + else { + sendStatus(HttpResponseStatus.FORBIDDEN, channel, request); + } + return true; + } + + private static boolean hasAccess(File result) { + // deny access to .htaccess files + return !result.isDirectory() && result.canRead() && !(result.isHidden() || result.getName().startsWith(".ht")); + } + } + + @Nullable + private static Project findProject(String projectName, boolean isCustomHost) { + // user can rename project directory, so, we should support this case - find project by base directory name + Project candidateByDirectoryName = null; + for (Project project : ProjectManager.getInstance().getOpenProjects()) { + String name = project.getName(); + // domain name is case-insensitive + if (!project.isDisposed() && ((isCustomHost || !SystemInfoRt.isFileSystemCaseSensitive) ? projectName.equalsIgnoreCase(name) : projectName.equals(name))) { + return project; + } + + if (candidateByDirectoryName == null && compareNameAndProjectBasePath(projectName, project)) { + candidateByDirectoryName = project; + } + } + return candidateByDirectoryName; + } + + public static boolean compareNameAndProjectBasePath(String projectName, Project project) { + String basePath = project.getBasePath(); + return basePath != null && basePath.length() > projectName.length() && basePath.endsWith(projectName) && basePath.charAt(basePath.length() - projectName.length() - 1) == '/'; + } +}
\ No newline at end of file |