mirror of https://github.com/apache/lucene.git
SOLR-5518: Move editing files to a separte request handler
git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1547251 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
4a1301c750
commit
8e6f865dee
|
@ -118,6 +118,9 @@ New Features
|
||||||
* SOLR-5506: Support docValues in CollationField and ICUCollationField.
|
* SOLR-5506: Support docValues in CollationField and ICUCollationField.
|
||||||
(Robert Muir)
|
(Robert Muir)
|
||||||
|
|
||||||
|
* SOLR-5518: Added EditFileRequestHandler to deal with security issues around modifying
|
||||||
|
solr configuration files.
|
||||||
|
|
||||||
* SOLR-5023: Add support for deleteInstanceDir to be passed from SolrJ for Core
|
* SOLR-5023: Add support for deleteInstanceDir to be passed from SolrJ for Core
|
||||||
Unload action. (Lyubov Romanchuk, shalin)
|
Unload action. (Lyubov Romanchuk, shalin)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,357 @@
|
||||||
|
/*
|
||||||
|
* 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 $";
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,21 +17,17 @@
|
||||||
|
|
||||||
package org.apache.solr.handler.admin;
|
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.cloud.ZkSolrResourceLoader;
|
||||||
import org.apache.solr.common.SolrException;
|
import org.apache.solr.common.SolrException;
|
||||||
import org.apache.solr.common.SolrException.ErrorCode;
|
import org.apache.solr.common.SolrException.ErrorCode;
|
||||||
import org.apache.solr.common.cloud.SolrZkClient;
|
import org.apache.solr.common.cloud.SolrZkClient;
|
||||||
import org.apache.solr.common.params.CommonParams;
|
import org.apache.solr.common.params.CommonParams;
|
||||||
import org.apache.solr.common.params.ModifiableSolrParams;
|
import org.apache.solr.common.params.ModifiableSolrParams;
|
||||||
import org.apache.solr.common.util.ContentStream;
|
import org.apache.solr.common.params.SolrParams;
|
||||||
import org.apache.solr.common.util.ContentStreamBase;
|
import org.apache.solr.common.util.ContentStreamBase;
|
||||||
import org.apache.solr.common.util.NamedList;
|
import org.apache.solr.common.util.NamedList;
|
||||||
import org.apache.solr.common.util.SimpleOrderedMap;
|
import org.apache.solr.common.util.SimpleOrderedMap;
|
||||||
import org.apache.solr.core.CoreContainer;
|
import org.apache.solr.core.CoreContainer;
|
||||||
import org.apache.solr.core.CoreDescriptor;
|
|
||||||
import org.apache.solr.core.SolrCore;
|
import org.apache.solr.core.SolrCore;
|
||||||
import org.apache.solr.core.SolrResourceLoader;
|
import org.apache.solr.core.SolrResourceLoader;
|
||||||
import org.apache.solr.handler.RequestHandlerBase;
|
import org.apache.solr.handler.RequestHandlerBase;
|
||||||
|
@ -46,12 +42,10 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -94,46 +88,16 @@ import java.util.Set;
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @since solr 1.3
|
* @since solr 1.3
|
||||||
*
|
|
||||||
*
|
|
||||||
* As of Solr 4.7, 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/file?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>
|
|
||||||
*
|
|
||||||
* Configuration files in ZooKeeper are supported.
|
|
||||||
*
|
|
||||||
* Writing files out, @since solr 4.7
|
|
||||||
*/
|
*/
|
||||||
public class ShowFileRequestHandler extends RequestHandlerBase
|
public class ShowFileRequestHandler extends RequestHandlerBase
|
||||||
{
|
{
|
||||||
|
|
||||||
protected static final Logger log = LoggerFactory
|
|
||||||
.getLogger(ShowFileRequestHandler.class);
|
|
||||||
|
|
||||||
public static final String HIDDEN = "hidden";
|
public static final String HIDDEN = "hidden";
|
||||||
public static final String USE_CONTENT_TYPE = "contentType";
|
public static final String USE_CONTENT_TYPE = "contentType";
|
||||||
|
|
||||||
protected Set<String> hiddenFiles;
|
protected Set<String> hiddenFiles;
|
||||||
|
|
||||||
private final static String OP_PARAM = "op";
|
protected static final Logger log = LoggerFactory
|
||||||
private final static String OP_WRITE = "write";
|
.getLogger(ShowFileRequestHandler.class);
|
||||||
private final static String OP_TEST = "test";
|
|
||||||
|
|
||||||
|
|
||||||
public ShowFileRequestHandler()
|
public ShowFileRequestHandler()
|
||||||
|
@ -144,22 +108,22 @@ public class ShowFileRequestHandler extends RequestHandlerBase
|
||||||
@Override
|
@Override
|
||||||
public void init(NamedList args) {
|
public void init(NamedList args) {
|
||||||
super.init( args );
|
super.init( args );
|
||||||
|
hiddenFiles = initHidden(invariants);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<String> initHidden(SolrParams invariants) {
|
||||||
|
|
||||||
|
Set<String> hiddenRet = new HashSet<String>();
|
||||||
// Build a list of hidden files
|
// Build a list of hidden files
|
||||||
hiddenFiles = new HashSet<String>();
|
if (invariants != null) {
|
||||||
if( invariants != null ) {
|
String[] hidden = invariants.getParams(HIDDEN);
|
||||||
String[] hidden = invariants.getParams( HIDDEN );
|
if (hidden != null) {
|
||||||
if( hidden != null ) {
|
for (String s : hidden) {
|
||||||
for( String s : hidden ) {
|
hiddenRet.add(s.toUpperCase(Locale.ROOT));
|
||||||
hiddenFiles.add( s.toUpperCase(Locale.ROOT) );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return hiddenRet;
|
||||||
|
|
||||||
public Set<String> getHiddenFiles()
|
|
||||||
{
|
|
||||||
return hiddenFiles;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -167,139 +131,11 @@ public class ShowFileRequestHandler extends RequestHandlerBase
|
||||||
throws InterruptedException, KeeperException, IOException {
|
throws InterruptedException, KeeperException, IOException {
|
||||||
|
|
||||||
CoreContainer coreContainer = req.getCore().getCoreDescriptor().getCoreContainer();
|
CoreContainer coreContainer = req.getCore().getCoreDescriptor().getCoreContainer();
|
||||||
String op = req.getParams().get(OP_PARAM);
|
|
||||||
if (op == null) {
|
|
||||||
if (coreContainer.isZooKeeperAware()) {
|
if (coreContainer.isZooKeeperAware()) {
|
||||||
showFromZooKeeper(req, rsp, coreContainer);
|
showFromZooKeeper(req, rsp, coreContainer);
|
||||||
} else {
|
} else {
|
||||||
showFromFileSystem(req, rsp);
|
showFromFileSystem(req, rsp);
|
||||||
}
|
}
|
||||||
} else 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('\\', '/');
|
|
||||||
if (isHiddenFile(req, rsp, fname, true) == false) {
|
|
||||||
if (coreContainer.isZooKeeperAware()) {
|
|
||||||
writeToZooKeeper(req, rsp);
|
|
||||||
} else {
|
|
||||||
writeToFileSystem(req, rsp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// See if we should deal with this file
|
|
||||||
|
|
||||||
private boolean isHiddenFile(SolrQueryRequest req, SolrQueryResponse rsp, String fnameIn, boolean reportError) {
|
|
||||||
String fname = fnameIn.toUpperCase(Locale.ROOT);
|
|
||||||
if (hiddenFiles.contains(fname) || hiddenFiles.contains("*")) {
|
|
||||||
if (reportError) {
|
|
||||||
log.error("Cannot access " + fname);
|
|
||||||
rsp.setException(new 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.indexOf("..") >= 0 || fname.startsWith(".")) {
|
|
||||||
if (reportError) {
|
|
||||||
log.error("Invalid path: " + fname);
|
|
||||||
rsp.setException(new SolrException(ErrorCode.FORBIDDEN, "Invalid path: " + fnameIn));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure that if the schema is managed, we don't allow editing. Don't really want to put
|
|
||||||
// this in the init since we're not entirely sure when the managed schema will get initialized relative to this
|
|
||||||
// handler.
|
|
||||||
SolrCore core = req.getCore();
|
|
||||||
IndexSchema schema = core.getLatestSchema();
|
|
||||||
if (schema instanceof ManagedIndexSchema) {
|
|
||||||
String managed = schema.getResourceName();
|
|
||||||
|
|
||||||
if (fname.equalsIgnoreCase(managed)) {
|
|
||||||
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".
|
|
||||||
|
|
||||||
private String getAdminFileFromZooKeeper(SolrQueryRequest req, SolrQueryResponse rsp, SolrZkClient zkClient)
|
|
||||||
throws KeeperException, InterruptedException {
|
|
||||||
String adminFile = null;
|
|
||||||
SolrCore core = req.getCore();
|
|
||||||
|
|
||||||
final ZkSolrResourceLoader loader = (ZkSolrResourceLoader) core
|
|
||||||
.getResourceLoader();
|
|
||||||
String confPath = loader.getCollectionZkPath();
|
|
||||||
|
|
||||||
String fname = req.getParams().get("file", null);
|
|
||||||
if (fname == null) {
|
|
||||||
adminFile = confPath;
|
|
||||||
} else {
|
|
||||||
fname = fname.replace('\\', '/'); // normalize slashes
|
|
||||||
if (isHiddenFile(req, rsp, fname, true)) {
|
|
||||||
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(ErrorCode.NOT_FOUND, "Can not find: "
|
|
||||||
+ adminFile));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return adminFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 = getAdminFileFromZooKeeper(req, rsp, zkClient);
|
|
||||||
ContentStream stream = getOneInputStream(req, rsp);
|
|
||||||
if (stream == null) {
|
|
||||||
return; // Error already in rsp.
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] data = IOUtils.toByteArray(new InputStreamReader(stream.getStream(), "UTF-8"), "UTF-8");
|
|
||||||
String fname = req.getParams().get("file", null);
|
|
||||||
if (OP_TEST.equals(req.getParams().get(OP_PARAM))) {
|
|
||||||
testReloadSuccess(req, rsp, stream);
|
|
||||||
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()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a list of files from ZooKeeper for from the path in the file= parameter.
|
// Get a list of files from ZooKeeper for from the path in the file= parameter.
|
||||||
|
@ -309,7 +145,7 @@ public class ShowFileRequestHandler extends RequestHandlerBase
|
||||||
|
|
||||||
SolrZkClient zkClient = coreContainer.getZkController().getZkClient();
|
SolrZkClient zkClient = coreContainer.getZkController().getZkClient();
|
||||||
|
|
||||||
String adminFile = getAdminFileFromZooKeeper(req, rsp, zkClient);
|
String adminFile = getAdminFileFromZooKeeper(req, rsp, zkClient, hiddenFiles);
|
||||||
|
|
||||||
if (adminFile == null) {
|
if (adminFile == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -321,7 +157,7 @@ public class ShowFileRequestHandler extends RequestHandlerBase
|
||||||
|
|
||||||
NamedList<SimpleOrderedMap<Object>> files = new SimpleOrderedMap<SimpleOrderedMap<Object>>();
|
NamedList<SimpleOrderedMap<Object>> files = new SimpleOrderedMap<SimpleOrderedMap<Object>>();
|
||||||
for (String f : children) {
|
for (String f : children) {
|
||||||
if (isHiddenFile(req, rsp, f, false)) {
|
if (isHiddenFile(req, rsp, f, false, hiddenFiles)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -352,185 +188,9 @@ public class ShowFileRequestHandler extends RequestHandlerBase
|
||||||
rsp.setHttpCaching(false);
|
rsp.setHttpCaching(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
ContentStream stream = getOneInputStream(req, rsp);
|
|
||||||
if (stream == null) {
|
|
||||||
return; // Error should already have been logged.
|
|
||||||
}
|
|
||||||
|
|
||||||
File adminFile = getAdminFileFromFileSystem(req, rsp);
|
|
||||||
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, stream);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
FileUtils.copyInputStreamToFile(stream.getStream(), adminFile);
|
|
||||||
log.info("Successfully saved file " + adminFile.getAbsolutePath() + " locally");
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean testReloadSuccess(SolrQueryRequest req, SolrQueryResponse rsp, ContentStream stream) {
|
|
||||||
// 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.copyInputStreamToFile(stream.getStream(),
|
|
||||||
new File(new File(coll, "conf"), req.getParams().get("file", null)));
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
private File getAdminFileFromFileSystem(SolrQueryRequest req, SolrQueryResponse rsp) {
|
|
||||||
File adminFile = null;
|
|
||||||
final SolrResourceLoader loader = req.getCore().getResourceLoader();
|
|
||||||
File configdir = new File( loader.getConfigDir() );
|
|
||||||
if (!configdir.exists()) {
|
|
||||||
// TODO: maybe we should just open it this way to start with?
|
|
||||||
try {
|
|
||||||
configdir = new File( loader.getClassLoader().getResource(loader.getConfigDir()).toURI() );
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
log.error("Can not access configuration directory!");
|
|
||||||
rsp.setException(new SolrException( ErrorCode.FORBIDDEN, "Can not access configuration directory!", e));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String fname = req.getParams().get("file", null);
|
|
||||||
if( fname == null ) {
|
|
||||||
adminFile = configdir;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
fname = fname.replace( '\\', '/' ); // normalize slashes
|
|
||||||
if( hiddenFiles.contains( fname.toUpperCase(Locale.ROOT) ) ) {
|
|
||||||
log.error("Can not access: "+ fname);
|
|
||||||
rsp.setException(new SolrException( ErrorCode.FORBIDDEN, "Can not access: "+fname ));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if( fname.indexOf( ".." ) >= 0 ) {
|
|
||||||
log.error("Invalid path: "+ fname);
|
|
||||||
rsp.setException(new SolrException( ErrorCode.FORBIDDEN, "Invalid path: "+fname ));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
adminFile = new File( configdir, fname );
|
|
||||||
}
|
|
||||||
return adminFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the file indicated (or the directory listing) from the local file system.
|
// Return the file indicated (or the directory listing) from the local file system.
|
||||||
private void showFromFileSystem(SolrQueryRequest req, SolrQueryResponse rsp) {
|
private void showFromFileSystem(SolrQueryRequest req, SolrQueryResponse rsp) {
|
||||||
File adminFile = getAdminFileFromFileSystem(req, rsp);
|
File adminFile = getAdminFileFromFileSystem(req, rsp, hiddenFiles);
|
||||||
|
|
||||||
if (adminFile == null) { // exception already recorded
|
if (adminFile == null) { // exception already recorded
|
||||||
return;
|
return;
|
||||||
|
@ -561,7 +221,7 @@ public class ShowFileRequestHandler extends RequestHandlerBase
|
||||||
String path = f.getAbsolutePath().substring( basePath );
|
String path = f.getAbsolutePath().substring( basePath );
|
||||||
path = path.replace( '\\', '/' ); // normalize slashes
|
path = path.replace( '\\', '/' ); // normalize slashes
|
||||||
|
|
||||||
if (isHiddenFile(req, rsp, f.getName().replace('\\', '/'), false)) {
|
if (isHiddenFile(req, rsp, f.getName().replace('\\', '/'), false, hiddenFiles)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -593,6 +253,127 @@ public class ShowFileRequestHandler extends RequestHandlerBase
|
||||||
rsp.setHttpCaching(false);
|
rsp.setHttpCaching(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////////////////////// Static methods //////////////////////////////
|
||||||
|
|
||||||
|
public static boolean isHiddenFile(SolrQueryRequest req, SolrQueryResponse rsp, String fnameIn, boolean reportError,
|
||||||
|
Set<String> 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.indexOf("..") >= 0 || fname.startsWith(".")) {
|
||||||
|
if (reportError) {
|
||||||
|
log.error("Invalid path: " + fname);
|
||||||
|
rsp.setException(new SolrException(SolrException.ErrorCode.FORBIDDEN, "Invalid path: " + fnameIn));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that if the schema is managed, we don't allow editing. Don't really want to put
|
||||||
|
// this in the init since we're not entirely sure when the managed schema will get initialized relative to this
|
||||||
|
// handler.
|
||||||
|
SolrCore core = req.getCore();
|
||||||
|
IndexSchema schema = core.getLatestSchema();
|
||||||
|
if (schema instanceof ManagedIndexSchema) {
|
||||||
|
String managed = schema.getResourceName();
|
||||||
|
|
||||||
|
if (fname.equalsIgnoreCase(managed)) {
|
||||||
|
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<String> hiddenFiles)
|
||||||
|
throws KeeperException, InterruptedException {
|
||||||
|
String adminFile = null;
|
||||||
|
SolrCore core = req.getCore();
|
||||||
|
|
||||||
|
final ZkSolrResourceLoader loader = (ZkSolrResourceLoader) core
|
||||||
|
.getResourceLoader();
|
||||||
|
String confPath = loader.getCollectionZkPath();
|
||||||
|
|
||||||
|
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 File getAdminFileFromFileSystem(SolrQueryRequest req, SolrQueryResponse rsp,
|
||||||
|
Set<String> hiddenFiles) {
|
||||||
|
File adminFile = null;
|
||||||
|
final SolrResourceLoader loader = req.getCore().getResourceLoader();
|
||||||
|
File configdir = new File( loader.getConfigDir() );
|
||||||
|
if (!configdir.exists()) {
|
||||||
|
// TODO: maybe we should just open it this way to start with?
|
||||||
|
try {
|
||||||
|
configdir = new File( 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 ) {
|
||||||
|
adminFile = configdir;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
if( fname.indexOf( ".." ) >= 0 ) {
|
||||||
|
log.error("Invalid path: "+ fname);
|
||||||
|
rsp.setException(new SolrException( SolrException.ErrorCode.FORBIDDEN, "Invalid path: "+fname ));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
adminFile = new File( configdir, fname );
|
||||||
|
}
|
||||||
|
return adminFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final Set<String> getHiddenFiles() {
|
||||||
|
return hiddenFiles;
|
||||||
|
}
|
||||||
|
|
||||||
//////////////////////// SolrInfoMBeans methods //////////////////////
|
//////////////////////// SolrInfoMBeans methods //////////////////////
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -92,6 +92,11 @@
|
||||||
</lst>
|
</lst>
|
||||||
</requestHandler>
|
</requestHandler>
|
||||||
|
|
||||||
|
<requestHandler name="/admin/fileedit" class="solr.admin.EditFileRequestHandler" >
|
||||||
|
<lst name="invariants">
|
||||||
|
<str name="hidden">bogus.txt</str>
|
||||||
|
</lst>
|
||||||
|
</requestHandler>
|
||||||
|
|
||||||
<updateRequestProcessorChain name="distrib-dup-test-chain-explicit">
|
<updateRequestProcessorChain name="distrib-dup-test-chain-explicit">
|
||||||
<!-- explicit test using processors before and after distrib -->
|
<!-- explicit test using processors before and after distrib -->
|
||||||
|
|
|
@ -240,6 +240,12 @@
|
||||||
</lst>
|
</lst>
|
||||||
</requestHandler>
|
</requestHandler>
|
||||||
|
|
||||||
|
<requestHandler name="/admin/fileedit" class="solr.admin.EditFileRequestHandler" >
|
||||||
|
<lst name="invariants">
|
||||||
|
<str name="hidden">bogus.txt</str>
|
||||||
|
</lst>
|
||||||
|
</requestHandler>
|
||||||
|
|
||||||
<!-- test query parameter defaults -->
|
<!-- test query parameter defaults -->
|
||||||
<requestHandler name="defaults" class="solr.StandardRequestHandler">
|
<requestHandler name="defaults" class="solr.StandardRequestHandler">
|
||||||
<lst name="defaults">
|
<lst name="defaults">
|
||||||
|
|
|
@ -16,6 +16,8 @@ package org.apache.solr.cloud;
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.apache.solr.SolrTestCaseJ4;
|
||||||
import org.apache.solr.client.solrj.impl.HttpSolrServer;
|
import org.apache.solr.client.solrj.impl.HttpSolrServer;
|
||||||
import org.apache.solr.client.solrj.request.QueryRequest;
|
import org.apache.solr.client.solrj.request.QueryRequest;
|
||||||
import org.apache.solr.common.cloud.SolrZkClient;
|
import org.apache.solr.common.cloud.SolrZkClient;
|
||||||
|
@ -23,6 +25,8 @@ import org.apache.solr.common.params.ModifiableSolrParams;
|
||||||
import org.apache.solr.common.util.NamedList;
|
import org.apache.solr.common.util.NamedList;
|
||||||
import org.apache.solr.common.util.SimpleOrderedMap;
|
import org.apache.solr.common.util.SimpleOrderedMap;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
public class TestModifyConfFiles extends AbstractFullDistribZkTestBase {
|
public class TestModifyConfFiles extends AbstractFullDistribZkTestBase {
|
||||||
|
|
||||||
public TestModifyConfFiles() {
|
public TestModifyConfFiles() {
|
||||||
|
@ -38,7 +42,7 @@ public class TestModifyConfFiles extends AbstractFullDistribZkTestBase {
|
||||||
params.set("op", "write");
|
params.set("op", "write");
|
||||||
params.set("file", "schema.xml");
|
params.set("file", "schema.xml");
|
||||||
QueryRequest request = new QueryRequest(params);
|
QueryRequest request = new QueryRequest(params);
|
||||||
request.setPath("/admin/file");
|
request.setPath("/admin/fileedit");
|
||||||
try {
|
try {
|
||||||
client.request(request);
|
client.request(request);
|
||||||
fail("Should have caught exception");
|
fail("Should have caught exception");
|
||||||
|
@ -50,7 +54,7 @@ public class TestModifyConfFiles extends AbstractFullDistribZkTestBase {
|
||||||
params.set("stream.body", "Testing rewrite of schema.xml file.");
|
params.set("stream.body", "Testing rewrite of schema.xml file.");
|
||||||
params.set("op", "test");
|
params.set("op", "test");
|
||||||
request = new QueryRequest(params);
|
request = new QueryRequest(params);
|
||||||
request.setPath("/admin/file");
|
request.setPath("/admin/fileedit");
|
||||||
try {
|
try {
|
||||||
client.request(request);
|
client.request(request);
|
||||||
fail("Should have caught exception");
|
fail("Should have caught exception");
|
||||||
|
@ -61,7 +65,7 @@ public class TestModifyConfFiles extends AbstractFullDistribZkTestBase {
|
||||||
params.set("op", "write");
|
params.set("op", "write");
|
||||||
params.set("file", "bogus.txt");
|
params.set("file", "bogus.txt");
|
||||||
request = new QueryRequest(params);
|
request = new QueryRequest(params);
|
||||||
request.setPath("/admin/file");
|
request.setPath("/admin/fileedit");
|
||||||
try {
|
try {
|
||||||
client.request(request);
|
client.request(request);
|
||||||
fail("Should have caught exception");
|
fail("Should have caught exception");
|
||||||
|
@ -69,16 +73,29 @@ public class TestModifyConfFiles extends AbstractFullDistribZkTestBase {
|
||||||
assertEquals(e.getMessage(), "Can not access: bogus.txt");
|
assertEquals(e.getMessage(), "Can not access: bogus.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
params.set("file", "schema.xml");
|
params.set("file", "schema.xml");
|
||||||
request = new QueryRequest(params);
|
request = new QueryRequest(params);
|
||||||
request.setPath("/admin/file");
|
request.setPath("/admin/fileedit");
|
||||||
|
client.request(request);
|
||||||
|
fail("Should have caught exception since it's mal-formed XML");
|
||||||
|
} catch (Exception e) {
|
||||||
|
assertTrue("Should have a sax parser exception here!",
|
||||||
|
e.getMessage().contains("Invalid XML file: org.xml.sax.SAXParseException"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String top = SolrTestCaseJ4.TEST_HOME() + "/collection1/conf";
|
||||||
|
params.set("stream.body", FileUtils.readFileToString(new File(top, "schema-tiny.xml"), "UTF-8"));
|
||||||
|
params.set("file", "schema.xml");
|
||||||
|
request = new QueryRequest(params);
|
||||||
|
request.setPath("/admin/fileedit");
|
||||||
|
|
||||||
client.request(request);
|
client.request(request);
|
||||||
|
|
||||||
SolrZkClient zkClient = cloudClient.getZkStateReader().getZkClient();
|
SolrZkClient zkClient = cloudClient.getZkStateReader().getZkClient();
|
||||||
String contents = new String(zkClient.getData("/configs/conf1/schema.xml", null, null, true), "UTF-8");
|
String contents = new String(zkClient.getData("/configs/conf1/schema.xml", null, null, true), "UTF-8");
|
||||||
|
|
||||||
assertTrue("Schema contents should have changed!", "Testing rewrite of schema.xml file.".equals(contents));
|
assertTrue("Schema contents should have changed!", contents.contains("<schema name=\"tiny\" version=\"1.1\">"));
|
||||||
|
|
||||||
// Create a velocity/whatever node. Put a bit of data in it. See if you can change it.
|
// Create a velocity/whatever node. Put a bit of data in it. See if you can change it.
|
||||||
zkClient.makePath("/configs/conf1/velocity/test.vm", false, true);
|
zkClient.makePath("/configs/conf1/velocity/test.vm", false, true);
|
||||||
|
@ -86,7 +103,7 @@ public class TestModifyConfFiles extends AbstractFullDistribZkTestBase {
|
||||||
params.set("stream.body", "Some bogus stuff for a test.");
|
params.set("stream.body", "Some bogus stuff for a test.");
|
||||||
params.set("file", "velocity/test.vm");
|
params.set("file", "velocity/test.vm");
|
||||||
request = new QueryRequest(params);
|
request = new QueryRequest(params);
|
||||||
request.setPath("/admin/file");
|
request.setPath("/admin/fileedit");
|
||||||
|
|
||||||
client.request(request);
|
client.request(request);
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import com.carrotsearch.randomizedtesting.rules.SystemPropertiesRestoreRule;
|
||||||
import org.apache.commons.codec.Charsets;
|
import org.apache.commons.codec.Charsets;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.solr.SolrTestCaseJ4;
|
import org.apache.solr.SolrTestCaseJ4;
|
||||||
|
import org.apache.solr.client.solrj.request.QueryRequest;
|
||||||
import org.apache.solr.common.params.ModifiableSolrParams;
|
import org.apache.solr.common.params.ModifiableSolrParams;
|
||||||
import org.apache.solr.common.util.ContentStream;
|
import org.apache.solr.common.util.ContentStream;
|
||||||
import org.apache.solr.common.util.ContentStreamBase;
|
import org.apache.solr.common.util.ContentStreamBase;
|
||||||
|
@ -41,6 +42,7 @@ import java.util.ArrayList;
|
||||||
|
|
||||||
public class ModifyConfFileTest extends SolrTestCaseJ4 {
|
public class ModifyConfFileTest extends SolrTestCaseJ4 {
|
||||||
private File solrHomeDirectory = new File(TEMP_DIR, this.getClass().getName());
|
private File solrHomeDirectory = new File(TEMP_DIR, this.getClass().getName());
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TestRule solrTestRules = RuleChain.outerRule(new SystemPropertiesRestoreRule());
|
public TestRule solrTestRules = RuleChain.outerRule(new SystemPropertiesRestoreRule());
|
||||||
|
|
||||||
|
@ -69,26 +71,42 @@ public class ModifyConfFileTest extends SolrTestCaseJ4 {
|
||||||
|
|
||||||
SolrCore core = cc.getCore("core1");
|
SolrCore core = cc.getCore("core1");
|
||||||
SolrQueryResponse rsp = new SolrQueryResponse();
|
SolrQueryResponse rsp = new SolrQueryResponse();
|
||||||
SolrRequestHandler handler = core.getRequestHandler("/admin/file");
|
SolrRequestHandler handler = core.getRequestHandler("/admin/fileedit");
|
||||||
|
|
||||||
ModifiableSolrParams params = params("file","schema.xml", "op","write");
|
ModifiableSolrParams params = params("file","schema.xml", "op","write");
|
||||||
core.execute(handler, new LocalSolrQueryRequest(core, params), rsp);
|
core.execute(handler, new LocalSolrQueryRequest(core, params), rsp);
|
||||||
assertEquals(rsp.getException().getMessage(), "Input stream list was null for admin file write operation.");
|
assertEquals(rsp.getException().getMessage(), "Input stream list was null for admin file write operation.");
|
||||||
|
|
||||||
params = params("op", "write", "stream.body", "Testing rewrite of schema.xml file.");
|
params = params("op", "write");
|
||||||
core.execute(handler, new LocalSolrQueryRequest(core, params), rsp);
|
core.execute(handler, new LocalSolrQueryRequest(core, params), rsp);
|
||||||
assertEquals(rsp.getException().getMessage(), "No file name specified for write operation.");
|
assertEquals(rsp.getException().getMessage(), "No file name specified for write operation.");
|
||||||
|
|
||||||
|
ArrayList<ContentStream> streams = new ArrayList<ContentStream>( 2 );
|
||||||
|
streams.add(new ContentStreamBase.StringStream("Testing rewrite of schema.xml file." ) );
|
||||||
|
|
||||||
params = params("op", "write", "file", "bogus.txt");
|
params = params("op", "write", "file", "bogus.txt");
|
||||||
core.execute(handler, new LocalSolrQueryRequest(core, params), rsp);
|
LocalSolrQueryRequest locReq = new LocalSolrQueryRequest(core, params);
|
||||||
|
locReq.setContentStreams(streams);
|
||||||
|
core.execute(handler, locReq, rsp);
|
||||||
assertEquals(rsp.getException().getMessage(), "Can not access: bogus.txt");
|
assertEquals(rsp.getException().getMessage(), "Can not access: bogus.txt");
|
||||||
|
|
||||||
ArrayList<ContentStream> streams = new ArrayList<ContentStream>( 2 );
|
String top = SolrTestCaseJ4.TEST_HOME() + "/collection1/conf";
|
||||||
streams.add( new ContentStreamBase.StringStream( "Testing rewrite of schema.xml file." ) );
|
String badConf = FileUtils.readFileToString(new File(top, "solrconfig-minimal.xml"), "UTF-8").replace("</dataDir>", "");
|
||||||
|
|
||||||
|
params = params("op", "write", "file", "solrconfig.xml");
|
||||||
|
locReq = new LocalSolrQueryRequest(core, params);
|
||||||
|
streams.clear();
|
||||||
|
streams.add(new ContentStreamBase.StringStream(badConf));
|
||||||
|
locReq.setContentStreams(streams);
|
||||||
|
core.execute(handler, locReq, rsp);
|
||||||
|
assertTrue("should have detected an error early!",
|
||||||
|
rsp.getException().getMessage().contains("\"dataDir\""));
|
||||||
|
|
||||||
|
assertTrue("should have detected an error early!",
|
||||||
|
rsp.getException().getMessage().contains("\"</dataDir>\""));
|
||||||
|
|
||||||
params = params("op", "test", "file", "schema.xml", "stream.body", "Testing rewrite of schema.xml file.");
|
params = params("op", "test", "file", "schema.xml", "stream.body", "Testing rewrite of schema.xml file.");
|
||||||
LocalSolrQueryRequest locReq = new LocalSolrQueryRequest(core, params);
|
locReq = new LocalSolrQueryRequest(core, params);
|
||||||
locReq.setContentStreams(streams);
|
locReq.setContentStreams(streams);
|
||||||
core.execute(handler, locReq, rsp);
|
core.execute(handler, locReq, rsp);
|
||||||
|
|
||||||
|
@ -116,7 +134,8 @@ public class ModifyConfFileTest extends SolrTestCaseJ4 {
|
||||||
streams.clear();
|
streams.clear();
|
||||||
params = params();
|
params = params();
|
||||||
locReq = new LocalSolrQueryRequest(core, params);
|
locReq = new LocalSolrQueryRequest(core, params);
|
||||||
core.execute(handler, locReq, rsp);
|
|
||||||
|
core.execute(core.getRequestHandler("/admin/file"), locReq, rsp);
|
||||||
|
|
||||||
NamedList<Object> res = rsp.getValues();
|
NamedList<Object> res = rsp.getValues();
|
||||||
|
|
||||||
|
|
|
@ -1139,6 +1139,19 @@
|
||||||
</requestHandler>
|
</requestHandler>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Enabling this request handler (which is NOT a default part of the admin handler) will allow the Solr UI to edit
|
||||||
|
all the config files. This is intended for secure/development use ONLY! Leaving available and publically
|
||||||
|
accessible is a security vulnerability and should be done with extreme caution!
|
||||||
|
-->
|
||||||
|
<!--
|
||||||
|
<requestHandler name="/admin/fileedit" class="solr.admin.EditFileRequestHandler" >
|
||||||
|
<lst name="invariants">
|
||||||
|
<str name="hidden">synonyms.txt</str>
|
||||||
|
<str name="hidden">anotherfile.txt</str>
|
||||||
|
</lst>
|
||||||
|
</requestHandler>
|
||||||
|
-->
|
||||||
<!-- ping/healthcheck -->
|
<!-- ping/healthcheck -->
|
||||||
<requestHandler name="/admin/ping" class="solr.PingRequestHandler">
|
<requestHandler name="/admin/ping" class="solr.PingRequestHandler">
|
||||||
<lst name="invariants">
|
<lst name="invariants">
|
||||||
|
|
Loading…
Reference in New Issue