| /* |
| * 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) == '/'; |
| } |
| } |