// Copyright 2008 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.enterprise.connector.servlet;
import com.google.enterprise.connector.logging.NDC;
import com.google.enterprise.connector.manager.Context;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* <p>Admin servlet to retrieve the configuration of the Connector Manager
* and all Connector instances. This servlet returns a ZIP archive of all
* the configuration files. Access to this servlet is restricted to either
* localhost or gsa.feed.host, based upon the HTTP RemoteAddress.</p>
*
* <p><b>Usage:</b>
* <br>To retrieve the zipped configuration archive:
* <br><pre> http://[cm_host_addr]/connector-manager/getConfig</pre>
* <br>or
* <br><pre> http://[cm_host_addr]/connector-manager/getConfig/configuration.zip</pre>
* </p>
*
* <p><br><b>Redirects and curl:</b>
* When requesting the configuration files, this servlet returns a redirect
* to the actual ZIP filename, configuration.zip.
* This allows the browser, wget, or curl to pull the true filename off
* the redirected URL so that it can name the file when storing locally.
* If using curl to retrieve the files, please to use 'curl -L' to tell
* curl to follow the redirect. Unfortunately, when using 'curl -O' to
* save the file locally, curl uses the pre-redirected name, rather than
* the post-redirected name when naming the local file. This forces you
* to use 'curl -L -o output_filename'.</p>
*
* <p>Wget handles redirects appropriately without intervention, and names
* the saved file as expected.</p>
*/
public class GetConfig extends HttpServlet {
private static final String archiveName = "configuration.zip";
private static final String[] excludedFiles =
{ "lib", "connector_manager.keystore", "web.xml" };
private static final FilenameFilter fileFilter = new ConfigFilenameFilter();
/**
* Retrieves the Configuration files for the Connector Manager and all the
* Connector instances.
*
* @param req
* @param res
* @throws IOException
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res)
throws IOException, FileNotFoundException {
doGet(req, res);
}
/**
* Retrieves the configuration data for the Connector Manager and all
* Connector instances.
*
* @param req
* @param res
* @throws IOException
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException, FileNotFoundException {
// Make sure this requester is OK
if (!RemoteAddressFilter.getInstance()
.allowed(RemoteAddressFilter.Access.RED, req.getRemoteAddr())) {
res.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
NDC.push("Support");
try {
Context context = Context.getInstance();
// Fetch the name of the archive file to return. getPathInfo() returns
// items with a leading '/', so we want to pull off only the basename.
// WARNING: For security reasons, the PathInfo parameter must never
// be passed directly to a File() or shell command.
String fileName = baseName(req.getPathInfo());
if (fileName == null) {
// Force a redirect to the archive file.
res.sendRedirect(res.encodeRedirectURL(
baseName(req.getServletPath()) + "/" + archiveName));
} else if (fileName.equalsIgnoreCase(archiveName)) {
res.setContentType(ServletUtil.MIMETYPE_ZIP);
ServletOutputStream out = res.getOutputStream();
try {
handleDoGet(context.getCommonDirPath(), out);
} finally {
out.close();
}
} else {
// Force a redirect to the archive file.
res.sendRedirect(res.encodeRedirectURL(archiveName));
}
} finally {
NDC.clear();
}
}
/**
* Specialized {@code doTrace} method that constructs an XML representation
* of the given request and returns it as the response.
*/
@Override
protected void doTrace(HttpServletRequest req, HttpServletResponse res)
throws IOException {
ServletDump.dumpServletRequest(req, res);
}
/**
* Handler for doGet in order to do unit tests.
*
* @param configDir root of configuration files
* @param out OutputStream where the response is written
* @throws IOException
* @throws FileNotFoundException
*/
public static void handleDoGet(String configDir, OutputStream out)
throws IOException, FileNotFoundException {
File dir = new File(configDir);
String relativeDir = dir.getParentFile().getCanonicalPath() + "/";
if (dir.exists() && dir.isDirectory()) {
//create a ZipOutputStream to zip the data to
ZipOutputStream zout = new ZipOutputStream(out);
zipDir(relativeDir, dir, zout);
//close the stream
zout.finish();
} else {
throw new FileNotFoundException("Configuration directory "
+ configDir + " not found.");
}
}
/**
* Add the directory and its contents to the ZipOutputStream
*
* @param relativeDir the directory make ZipEntries relative to.
* @param dir the directory to add to the ZIP file.
* @param zout a ZipOutputStream
* @throws IOException
*/
public static void zipDir(String relativeDir, File dir, ZipOutputStream zout)
throws IOException {
// Get a listing of the directory content.
String[] dirList = dir.list(fileFilter);
// Loop through dirList, and zip the files.
for (int i = 0; i < dirList.length; i++) {
File file = new File(dir, dirList[i]);
if (file.isDirectory()) {
// If the File object is a directory, call this
// function again to add its content recursively.
zipDir(relativeDir, file, zout);
continue;
}
// We have plain file, not a directory.
// Add a ZIP entry describing the file.
String relativePath = file.getCanonicalPath();
if (relativePath.startsWith(relativeDir)) {
relativePath = relativePath.substring(relativeDir.length());
}
ZipEntry zentry = new ZipEntry(relativePath);
zentry.setSize(file.length());
zentry.setTime(file.lastModified());
zout.putNextEntry(zentry);
// Copy the file to the compressed stream.
InputStream in = new FileInputStream(file);
byte[] buf = new byte[16384];
int bytesRead;
while ((bytesRead = in.read(buf)) > 0) {
zout.write(buf, 0, bytesRead);
}
in.close();
zout.closeEntry();
}
}
/**
* Return the base filename part of the pattern or log name.
* For instance "/x/y/z" returns "z", "/x/y/" returns "".
*
* @param name unix-style pathname or pattern.
* @return the base filename (may be null or empty)
*/
private static String baseName(String name) {
if (name != null) {
// FileHandler patterns use '/' as separatorChar by default.
int sep = name.lastIndexOf('/');
// If no '/', then look for system separatorChar.
if ((sep == -1) && (File.separatorChar != '/')) {
sep = name.lastIndexOf(File.separatorChar);
}
return name.substring(sep + 1);
}
return null;
}
/**
* This is a FilenameFilter that filters out configuration
* files that we do not wish to return.
*/
private static class ConfigFilenameFilter implements FilenameFilter {
/**
* Tests if the specified file matches one of the files to exclude.
*
* @param dir the directory containing the file.
* @param fileName a file in the directory.
* @returns true if the fileName is not excluded, false otherwise.
*/
public boolean accept(File dir, String fileName) {
// Ignore ".", "..", hidden files, and old file versions.
if (fileName.startsWith(".") || fileName.endsWith("~")) {
return false;
}
// Iterate over the excluded filenames, looking for a match.
for (int i = 0; i < excludedFiles.length; i++) {
if (fileName.equalsIgnoreCase(excludedFiles[i])) {
return false;
}
}
return true;
}
}
}