/* * 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 org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.solr.cloud.ZkController; 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.util.ContentStream; import org.apache.solr.common.util.NamedList; import org.apache.solr.core.Config; import org.apache.solr.core.CoreContainer; import org.apache.solr.core.CoreDescriptor; import org.apache.solr.core.SolrConfig; import org.apache.solr.core.SolrCore; 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.zookeeper.KeeperException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.InputSource; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.util.HashSet; import java.util.Iterator; import java.util.Locale; import java.util.Set; /** * This handler uses the RawResponseWriter to give client access to * files inside ${solr.home}/conf * <p/> * If you want to selectively restrict access some configuration files, you can list * these files in the hidden invariants. For example to hide * synonyms.txt and anotherfile.txt, you would register: * <p/> * <pre> * <requestHandler name="/admin/fileupdate" class="org.apache.solr.handler.admin.EditFileRequestHandler" > * <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> * </pre> * <p/> * At present, there is only explicit file names (including path) or the glob '*' are supported. Variants like '*.xml' * are NOT supported.ere * <p/> * <p/> * The EditFileRequestHandler 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. * <p/> * If you want to override the contentType header returned for a given file, you can * set it directly using: CONTENT_TYPE. For example, to get a plain text * version of schema.xml, try: * <pre> * http://localhost:8983/solr/admin/fileedit?file=schema.xml&contentType=text/plain * </pre> * * @since solr 4.7 * <p/> * <p/> * You can use this handler to modify any files in the conf directory, e.g. solrconfig.xml * or schema.xml, or even in sub-directories (e.g. velocity/error.vm) by POSTing a file. Here's an example cURL command * <pre> * curl -X POST --form "fileupload=@schema.new" 'http://localhost:8983/solr/collection1/admin/fileedit?op=write&file=schema.xml' * </pre> * * or * <pre> * curl -X POST --form "fileupload=@error.new" 'http://localhost:8983/solr/collection1/admin/file?op=write&file=velocity/error.vm' * </pre> * * For the first iteration, this is probably going to be used from the Solr admin screen. * * NOTE: Specifying a directory or simply leaving the any "file=XXX" parameters will list the contents of a directory. * * NOTE: <b>You must reload the core/collection for any changes made via this handler to take effect!</b> * * NOTE: <b>If the core does not load (say schema.xml is not well formed for instance) you may be unable to replace * the files with this interface.</b> * * NOTE: <b>Leaving this handler enabled is a security risk! This handler should be disabled in all but trusted * (probably development only) environments!</b> * * Configuration files in ZooKeeper are supported. */ public class EditFileRequestHandler extends RequestHandlerBase { protected static final Logger log = LoggerFactory.getLogger(EditFileRequestHandler.class); private final static String OP_PARAM = "op"; private final static String OP_WRITE = "write"; private final static String OP_TEST = "test"; ContentStream stream; private byte[] data = null; Set<String> hiddenFiles; public EditFileRequestHandler() { super(); } @Override public void init(NamedList args) { super.init(args); hiddenFiles = ShowFileRequestHandler.initHidden(invariants); } @Override public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws InterruptedException, KeeperException, IOException { CoreContainer coreContainer = req.getCore().getCoreDescriptor().getCoreContainer(); String op = req.getParams().get(OP_PARAM); if (OP_WRITE.equalsIgnoreCase(op) || OP_TEST.equalsIgnoreCase(op)) { String fname = req.getParams().get("file", null); if (fname == null) { rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "No file name specified for write operation.")); } else { fname = fname.replace('\\', '/'); stream = getOneInputStream(req, rsp); if (stream == null) { return; // Error already in rsp. } data = IOUtils.toByteArray(new InputStreamReader(stream.getStream(), "UTF-8"), "UTF-8"); // If it's "solrconfig.xml", try parsing it as that object. Otherwise, if it ends in '.xml', // see if it at least parses. if ("solrconfig.xml".equals(fname)) { try { new SolrConfig("unused", new InputSource(new ByteArrayInputStream(data))); } catch (Exception e) { rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "Invalid solr config file: " + e.getMessage())); return; } } else if (fname.endsWith(".xml")) { // At least do a rudimentary test, see if the thing parses. try { new Config(null, null, new InputSource(new ByteArrayInputStream(data)), null, false); } catch (Exception e) { rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "Invalid XML file: " + e.getMessage())); return; } } if (ShowFileRequestHandler.isHiddenFile(req, rsp, fname, true, hiddenFiles) == false) { if (coreContainer.isZooKeeperAware()) { writeToZooKeeper(req, rsp); } else { writeToFileSystem(req, rsp); } } } } } // write the file contained in the parameter "file=XXX" to ZooKeeper. The file may be a path, e.g. // file=velocity/error.vm or file=schema.xml // // Important: Assumes that the file already exists in ZK, so far we aren't creating files there. private void writeToZooKeeper(SolrQueryRequest req, SolrQueryResponse rsp) throws KeeperException, InterruptedException, IOException { CoreContainer coreContainer = req.getCore().getCoreDescriptor().getCoreContainer(); SolrZkClient zkClient = coreContainer.getZkController().getZkClient(); String adminFile = ShowFileRequestHandler.getAdminFileFromZooKeeper(req, rsp, zkClient, hiddenFiles); String fname = req.getParams().get("file", null); if (OP_TEST.equals(req.getParams().get(OP_PARAM))) { testReloadSuccess(req, rsp); return; } // Persist the managed schema try { // Assumption: the path exists zkClient.setData(adminFile, data, true); log.info("Saved " + fname + " to ZooKeeper successfully."); } catch (KeeperException.BadVersionException e) { log.error("Cannot save file: " + fname + " to Zookeeper, " + "ZooKeeper error: " + e.getMessage()); rsp.setException(new SolrException(ErrorCode.SERVER_ERROR, "Cannot save file: " + fname + " to Zookeeper, " + "ZooKeeper error: " + e.getMessage())); } } // Used when POSTing the configuration files to Solr (either ZooKeeper or locally). // // It takes some effort to insure that there is one (and only one) stream provided, there's no provision for // more than one stream at present. private ContentStream getOneInputStream(SolrQueryRequest req, SolrQueryResponse rsp) { String file = req.getParams().get("file"); if (file == null) { log.error("You must specify a file for the write operation."); rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "You must specify a file for the write operation.")); return null; } // Now, this is truly clumsy Iterable<ContentStream> streams = req.getContentStreams(); if (streams == null) { log.error("Input stream list was null for admin file write operation."); rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "Input stream list was null for admin file write operation.")); return null; } Iterator<ContentStream> iter = streams.iterator(); if (!iter.hasNext()) { log.error("No input streams were in the list for admin file write operation."); rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "No input streams were in the list for admin file write operation.")); return null; } ContentStream stream = iter.next(); if (iter.hasNext()) { log.error("More than one input stream was found for admin file write operation."); rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "More than one input stream was found for admin file write operation.")); return null; } return stream; } // Write the data passed in from the stream to the file indicated by the file=XXX parameter on the local file system private void writeToFileSystem(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException { File adminFile = ShowFileRequestHandler.getAdminFileFromFileSystem(req, rsp, hiddenFiles); if (adminFile == null || adminFile.isDirectory()) { String fname = req.getParams().get("file", null); if (adminFile == null) { log.error("File " + fname + " was not found."); rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "File " + fname + " was not found.")); return; } log.error("File " + fname + " is a directory."); rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "File " + fname + " is a directory.")); return; } if (OP_TEST.equals(req.getParams().get(OP_PARAM))) { testReloadSuccess(req, rsp); return; } FileUtils.copyInputStreamToFile(stream.getStream(), adminFile); log.info("Successfully saved file " + adminFile.getAbsolutePath() + " locally"); } private boolean testReloadSuccess(SolrQueryRequest req, SolrQueryResponse rsp) { // Try writing the config to a temporary core and reloading to see that we don't allow people to shoot themselves // in the foot. File home = null; try { home = new File(FileUtils.getTempDirectory(), "SOLR_5459"); // Unlikely to name a core or collection this! FileUtils.writeStringToFile(new File(home, "solr.xml"), "<solr></solr>", "UTF-8"); // Use auto-discovery File coll = new File(home, "SOLR_5459"); SolrCore core = req.getCore(); CoreDescriptor desc = core.getCoreDescriptor(); CoreContainer coreContainer = desc.getCoreContainer(); if (coreContainer.isZooKeeperAware()) { try { String confPath = ((ZkSolrResourceLoader) core.getResourceLoader()).getCollectionZkPath(); ZkController.downloadConfigDir(coreContainer.getZkController().getZkClient(), confPath, new File(coll, "conf")); } catch (Exception ex) { log.error("Error when attempting to download conf from ZooKeeper: " + ex.getMessage()); rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "Error when attempting to download conf from ZooKeeper" + ex.getMessage())); return false; } } else { FileUtils.copyDirectory(new File(desc.getInstanceDir(), "conf"), new File(coll, "conf")); } FileUtils.writeStringToFile(new File(coll, "core.properties"), "name=SOLR_5459", "UTF-8"); FileUtils.writeByteArrayToFile(new File(new File(coll, "conf"), req.getParams().get("file", null)), data); return tryReloading(rsp, home); } catch (IOException ex) { log.warn("Caught IO exception when trying to verify configs. " + ex.getMessage()); rsp.setException(new SolrException(ErrorCode.SERVER_ERROR, "Caught IO exception when trying to verify configs. " + ex.getMessage())); return false; } finally { if (home != null) { try { FileUtils.deleteDirectory(home); } catch (IOException e) { log.warn("Caught IO exception trying to delete temporary directory " + home + e.getMessage()); return true; // Don't fail for this reason! } } } } private boolean tryReloading(SolrQueryResponse rsp, File home) { CoreContainer cc = null; try { cc = CoreContainer.createAndLoad(home.getAbsolutePath(), new File(home, "solr.xml")); if (cc.getCoreInitFailures().size() > 0) { for (Exception ex : cc.getCoreInitFailures().values()) { log.error("Error when attempting to reload core: " + ex.getMessage()); rsp.setException(new SolrException(ErrorCode.BAD_REQUEST, "Error when attempting to reload core after writing config" + ex.getMessage())); } return false; } return true; } finally { if (cc != null) { cc.shutdown(); } } } //////////////////////// SolrInfoMBeans methods ////////////////////// @Override public String getDescription() { return "Admin Config File -- update config files directly"; } @Override public String getSource() { return "$URL: https://svn.apache.org/repos/asf/lucene/dev/trunk/solr/core/src/java/org/apache/solr/handler/admin/ShowFileRequestHandler.java $"; } }