/* * Licensed to Crate under one or more contributor license agreements. * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. Crate licenses this file * to you 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. * * However, if you have executed another commercial license agreement * with Crate these terms will supersede the license and you may use the * software solely pursuant to the terms of the relevant commercial * agreement. */ package io.crate.rest.action.admin; import com.google.common.collect.ImmutableMap; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.FileSystemUtils; import org.elasticsearch.env.Environment; import org.elasticsearch.rest.*; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.util.Locale; import java.util.Map; import java.util.regex.Pattern; import static java.nio.file.Files.readAttributes; import static org.elasticsearch.rest.RestStatus.*; /** * RestFilter for admin ui requests * Serves admin ui files * * /index.html => serve index.html * / && isBrowser => serve index.html * / && isBrowser && Accept: application/json => don't serve index.html, continue processing * / && !isBrowser => don't serve any file, continue processing * /static/ => serve static file */ public class AdminUIStaticFileRequestFilter extends RestFilter { private final Environment environment; private static final Pattern USER_AGENT_BROWSER_PATTERN = Pattern.compile("(Mozilla|Chrome|Safari|Opera|Android|AppleWebKit)+?[/\\s][\\d.]+"); @Inject public AdminUIStaticFileRequestFilter(Environment environment) { this.environment = environment; } @Override public void process(RestRequest request, RestChannel channel, NodeClient client, RestFilterChain filterChain) throws IOException { if (request.rawPath().equals("/_plugin/crate-admin")){ BytesRestResponse resp = new BytesRestResponse(RestStatus.MOVED_PERMANENTLY, "Admin-UI location moved"); resp.addHeader("Location", "/"); channel.sendResponse(resp); return; } if (request.rawPath().equals("/index.html") || request.rawPath().startsWith("/static/") || shouldServeFromRoot(request)) { serveSite(request, channel); } else { filterChain.continueProcessing(request, channel, client); } } static boolean isBrowser(String headerValue) { if (headerValue == null){ return false; } String engine = headerValue.split("\\s+")[0]; return USER_AGENT_BROWSER_PATTERN.matcher(engine).matches(); } private static boolean shouldServeFromRoot(RestRequest request) { return request.rawPath().equals("/") && isBrowser(request.header("user-agent")) && !isAcceptJson(request.header("accept")); } static boolean isAcceptJson(String headerValue) { return headerValue != null && headerValue.contains("application/json"); } private void serveSite(RestRequest request, RestChannel channel) throws IOException { if (request.method() != RestRequest.Method.GET) { channel.sendResponse(new BytesRestResponse(FORBIDDEN, "GET is the only allowed method")); return; } String sitePath = request.rawPath(); while (sitePath.length() > 0 && sitePath.charAt(0) == '/') { sitePath = sitePath.substring(1); } // we default to index.html, or what the plugin provides (as a unix-style path) // this is a relative path under _site configured by the plugin. if (sitePath.length() == 0) { sitePath = "index.html"; } final Path siteFile = environment.pluginsFile().resolve("crate-admin").resolve("_site"); final String separator = siteFile.getFileSystem().getSeparator(); // Convert file separators. sitePath = sitePath.replace("/", separator); Path file = siteFile.resolve(sitePath); // return not found instead of forbidden to prevent malicious requests to find out if files exist or don't exist if (!Files.exists(file) || FileSystemUtils.isHidden(file) || !file.toAbsolutePath().normalize().startsWith(siteFile.toAbsolutePath().normalize())) { final String msg = "Requested file [" + file + "] was not found"; channel.sendResponse(new BytesRestResponse(NOT_FOUND, msg)); return; } BasicFileAttributes attributes = readAttributes(file, BasicFileAttributes.class); if (!attributes.isRegularFile()) { // If it's not a regular file, we send a 403 final String msg = "Requested file [" + file + "] is not a valid file."; channel.sendResponse(new BytesRestResponse(FORBIDDEN, msg)); return; } try { byte[] data = Files.readAllBytes(file); channel.sendResponse(new BytesRestResponse(OK, guessMimeType(file.toAbsolutePath().toString()), data)); } catch (IOException e) { channel.sendResponse(new BytesRestResponse(INTERNAL_SERVER_ERROR, e.getMessage())); } } private static String guessMimeType(String path) { int lastDot = path.lastIndexOf('.'); if (lastDot == -1) { return ""; } String extension = path.substring(lastDot + 1).toLowerCase(Locale.ROOT); String mimeType = DEFAULT_MIME_TYPES.get(extension); if (mimeType == null) { return ""; } return mimeType; } private static final Map<String, String> DEFAULT_MIME_TYPES = new ImmutableMap.Builder<String, String>() .put("txt", "text/plain") .put("css", "text/css") .put("csv", "text/csv") .put("htm", "text/html") .put("html", "text/html") .put("xml", "text/xml") .put("js", "text/javascript") // Technically it should be application/javascript (RFC 4329), but IE8 struggles with that .put("xhtml", "application/xhtml+xml") .put("json", "application/json") .put("pdf", "application/pdf") .put("zip", "application/zip") .put("tar", "application/x-tar") .put("gif", "image/gif") .put("jpeg", "image/jpeg") .put("jpg", "image/jpeg") .put("tiff", "image/tiff") .put("tif", "image/tiff") .put("png", "image/png") .put("svg", "image/svg+xml") .put("ico", "image/vnd.microsoft.icon") .put("mp3", "audio/mpeg") .build(); }