All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.solr.handler.admin.ShowFileRequestHandler Maven / Gradle / Ivy

There is a newer version: 9.7.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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.
 */
package org.apache.solr.handler.admin;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.invoke.MethodHandles;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.apache.solr.cloud.ZkSolrResourceLoader;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.ContentStreamBase;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.SolrCore;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.RawResponseWriter;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.security.AuthorizationContext;
import org.apache.solr.security.PermissionNameProvider;
import org.apache.zookeeper.KeeperException;
import org.eclipse.jetty.http.MimeTypes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This handler uses the RawResponseWriter to give client access to files inside ${solr.home}/conf
 *
 * 

If you want to selectively restrict access some configuration files, you can list these files * in the {@link #HIDDEN} invariants. For example to hide synonyms.txt and anotherfile.txt, you * would register:
* *

 * <requestHandler name="/admin/file" class="org.apache.solr.handler.admin.ShowFileRequestHandler" >
 *   <lst name="defaults">
 *    <str name="echoParams">explicit</str>
 *   </lst>
 *   <lst name="invariants">
 *    <str name="hidden">synonyms.txt</str>
 *    <str name="hidden">anotherfile.txt</str>
 *    <str name="hidden">*</str>
 *   </lst>
 * </requestHandler>
 * 
* * At present, there is only explicit file names (including path) or the glob '*' are supported. * Variants like '*.xml' are NOT supported.ere * *

The ShowFileRequestHandler uses the {@link RawResponseWriter} (wt=raw) to return file * contents. If you need to use a different writer, you will need to change the registered invariant * param for wt. * *

If you want to override the contentType header returned for a given file, you can set it * directly using: {@link #USE_CONTENT_TYPE}. For example, to get a plain text version of * schema.xml, try: * *

 *   http://localhost:8983/solr/admin/file?file=schema.xml&contentType=text/plain
 * 
* * @since solr 1.3 */ public class ShowFileRequestHandler extends RequestHandlerBase implements PermissionNameProvider { public static final String HIDDEN = "hidden"; public static final String USE_CONTENT_TYPE = "contentType"; private static final Set KNOWN_MIME_TYPES; protected Set hiddenFiles; private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); static { KNOWN_MIME_TYPES = new HashSet<>(MimeTypes.getKnownMimeTypes()); KNOWN_MIME_TYPES.add("text/xml"); KNOWN_MIME_TYPES.add("text/javascript"); } public ShowFileRequestHandler() { super(); } @Override public void init(NamedList args) { super.init(args); hiddenFiles = initHidden(invariants); } public static Set initHidden(SolrParams invariants) { Set hiddenRet = new HashSet<>(); // Build a list of hidden files if (invariants != null) { String[] hidden = invariants.getParams(HIDDEN); if (hidden != null) { for (String s : hidden) { hiddenRet.add(s.toUpperCase(Locale.ROOT)); } } } return hiddenRet; } @Override public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws InterruptedException, KeeperException, IOException { CoreContainer coreContainer = req.getCoreContainer(); if (coreContainer.isZooKeeperAware()) { showFromZooKeeper(req, rsp, coreContainer); } else { showFromFileSystem(req, rsp); } } // Get a list of files from ZooKeeper for from the path in the file= parameter. private void showFromZooKeeper( SolrQueryRequest req, SolrQueryResponse rsp, CoreContainer coreContainer) throws KeeperException, InterruptedException, UnsupportedEncodingException { SolrZkClient zkClient = coreContainer.getZkController().getZkClient(); String adminFile = getAdminFileFromZooKeeper(req, rsp, zkClient, hiddenFiles); if (adminFile == null) { return; } // Show a directory listing List children = zkClient.getChildren(adminFile, null, true); if (children.size() > 0) { NamedList> files = new SimpleOrderedMap<>(); for (String f : children) { if (isHiddenFile(req, rsp, f, false, hiddenFiles)) { continue; } SimpleOrderedMap fileInfo = new SimpleOrderedMap<>(); files.add(f, fileInfo); List fchildren = zkClient.getChildren(adminFile + "/" + f, null, true); if (fchildren.size() > 0) { fileInfo.add("directory", true); } else { // TODO? content type fileInfo.add("size", f.length()); } // TODO: ? // fileInfo.add( "modified", new Date( f.lastModified() ) ); } rsp.add("files", files); } else { // Include the file contents // The file logic depends on RawResponseWriter, so force its use. ModifiableSolrParams params = new ModifiableSolrParams(req.getParams()); params.set(CommonParams.WT, "raw"); req.setParams(params); ContentStreamBase content = new ContentStreamBase.ByteArrayStream( zkClient.getData(adminFile, null, null, true), adminFile); content.setContentType(getSafeContentType(req.getParams().get(USE_CONTENT_TYPE))); rsp.add(RawResponseWriter.CONTENT, content); } rsp.setHttpCaching(false); } // Return the file indicated (or the directory listing) from the local file system. private void showFromFileSystem(SolrQueryRequest req, SolrQueryResponse rsp) { Path admin = getAdminFileFromFileSystem(req, rsp, hiddenFiles); if (admin == null) { // exception already recorded return; } File adminFile = admin.toFile(); // Make sure the file exists, is readable and is not a hidden file if (!adminFile.exists()) { log.error("Can not find: {} [{}]", adminFile.getName(), adminFile.getAbsolutePath()); rsp.setException( new SolrException( ErrorCode.NOT_FOUND, "Can not find: " + adminFile.getName() + " [" + adminFile.getAbsolutePath() + "]")); return; } if (!adminFile.canRead() || adminFile.isHidden()) { log.error("Can not show: {} [{}]", adminFile.getName(), adminFile.getAbsolutePath()); rsp.setException( new SolrException( ErrorCode.NOT_FOUND, "Can not show: " + adminFile.getName() + " [" + adminFile.getAbsolutePath() + "]")); return; } // Show a directory listing if (adminFile.isDirectory()) { // it's really a directory, just go for it. int basePath = adminFile.getAbsolutePath().length() + 1; NamedList> files = new SimpleOrderedMap<>(); for (File f : adminFile.listFiles()) { String path = f.getAbsolutePath().substring(basePath); path = path.replace('\\', '/'); // normalize slashes if (isHiddenFile(req, rsp, f.getName().replace('\\', '/'), false, hiddenFiles)) { continue; } SimpleOrderedMap fileInfo = new SimpleOrderedMap<>(); files.add(path, fileInfo); if (f.isDirectory()) { fileInfo.add("directory", true); } else { // TODO? content type fileInfo.add("size", f.length()); } fileInfo.add("modified", new Date(f.lastModified())); } rsp.add("files", files); } else { // Include the file contents // The file logic depends on RawResponseWriter, so force its use. ModifiableSolrParams params = new ModifiableSolrParams(req.getParams()); params.set(CommonParams.WT, "raw"); req.setParams(params); ContentStreamBase content = new ContentStreamBase.FileStream(adminFile); content.setContentType(getSafeContentType(req.getParams().get(USE_CONTENT_TYPE))); rsp.add(RawResponseWriter.CONTENT, content); } rsp.setHttpCaching(false); } /** * Checks content type string and returns it if it is one of allowed types. The allowed types are * all standard mime types. If an HTML type is requested, it is instead returned as text/plain */ public static String getSafeContentType(String contentType) { if (StrUtils.isNullOrEmpty(contentType)) { log.debug("No contentType specified"); return null; } String rawContentType = contentType.split(";")[0].trim().toLowerCase(Locale.ROOT); // Strip away charset part if (!KNOWN_MIME_TYPES.contains(rawContentType)) { throw new SolrException( ErrorCode.BAD_REQUEST, "Requested content type '" + contentType + "' is not supported."); } if (rawContentType.contains("html")) { log.info("Using text/plain instead of {}", contentType); return "text/plain"; } return contentType; } //////////////////////// Static methods ////////////////////////////// public static boolean isHiddenFile( SolrQueryRequest req, SolrQueryResponse rsp, String fnameIn, boolean reportError, Set hiddenFiles) { String fname = fnameIn.toUpperCase(Locale.ROOT); if (hiddenFiles.contains(fname) || hiddenFiles.contains("*")) { if (reportError) { log.error("Cannot access {}", fname); rsp.setException( new SolrException(SolrException.ErrorCode.FORBIDDEN, "Can not access: " + fnameIn)); } return true; } // This is slightly off, a valid path is something like ./schema.xml. I don't think it's worth // the effort though to fix it to handle all possibilities though. if (fname.contains("..") || fname.startsWith(".")) { if (reportError) { log.error("Invalid path: {}", fname); rsp.setException( new SolrException(SolrException.ErrorCode.FORBIDDEN, "Invalid path: " + fnameIn)); } return true; } return false; } // Refactored to be usable from multiple methods. Gets the path of the requested file from ZK. // Returns null if the file is not found. // // Assumes that the file is in a parameter called "file". public static String getAdminFileFromZooKeeper( SolrQueryRequest req, SolrQueryResponse rsp, SolrZkClient zkClient, Set hiddenFiles) throws KeeperException, InterruptedException { String adminFile = null; SolrCore core = req.getCore(); final ZkSolrResourceLoader loader = (ZkSolrResourceLoader) core.getResourceLoader(); String confPath = loader.getConfigSetZkPath(); String fname = req.getParams().get("file", null); if (fname == null) { adminFile = confPath; } else { fname = fname.replace('\\', '/'); // normalize slashes if (isHiddenFile(req, rsp, fname, true, hiddenFiles)) { return null; } if (fname.startsWith("/")) { // Only files relative to conf are valid fname = fname.substring(1); } adminFile = confPath + "/" + fname; } // Make sure the file exists, is readable and is not a hidden file if (!zkClient.exists(adminFile, true)) { log.error("Can not find: {}", adminFile); rsp.setException( new SolrException(SolrException.ErrorCode.NOT_FOUND, "Can not find: " + adminFile)); return null; } return adminFile; } // Find the file indicated by the "file=XXX" parameter or the root of the conf directory on the // local file system. Respects all the "interesting" stuff around what the resource loader does to // find files. public static Path getAdminFileFromFileSystem( SolrQueryRequest req, SolrQueryResponse rsp, Set hiddenFiles) { final SolrResourceLoader loader = req.getCore().getResourceLoader(); Path configDir = loader.getConfigPath(); if (!Files.exists(configDir)) { // TODO: maybe we should just open it this way to start with? try { configDir = Path.of(loader.getClassLoader().getResource(loader.getConfigDir()).toURI()); } catch (URISyntaxException e) { log.error("Can not access configuration directory!"); rsp.setException( new SolrException( SolrException.ErrorCode.FORBIDDEN, "Can not access configuration directory!", e)); return null; } } String fname = req.getParams().get("file", null); if (fname == null) { return configDir; } fname = fname.replace('\\', '/'); // normalize slashes if (hiddenFiles.contains(fname.toUpperCase(Locale.ROOT))) { log.error("Can not access: {}", fname); rsp.setException( new SolrException(SolrException.ErrorCode.FORBIDDEN, "Can not access: " + fname)); return null; } // A leading slash is unnecessary but supported and interpreted as start of config dir Path filePath = configDir.resolve(fname.startsWith("/") ? fname.substring(1) : fname); req.getCoreContainer().assertPathAllowed(filePath); if (!filePath.normalize().startsWith(configDir.normalize())) { log.error("Path must be inside core config directory"); rsp.setException( new SolrException(ErrorCode.BAD_REQUEST, "Path must be inside core config directory")); return null; } return filePath; } public final Set getHiddenFiles() { return hiddenFiles; } //////////////////////// SolrInfoMBeans methods ////////////////////// @Override public String getDescription() { return "Admin Config File -- view or update config files directly"; } @Override public Category getCategory() { return Category.ADMIN; } @Override public Name getPermissionName(AuthorizationContext request) { return Name.CONFIG_READ_PERM; } }