/*
* RHQ Management Platform
* Copyright (C) 2005-2010 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.gui.content;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.servlets.DefaultServlet;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.net.URLCodec;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.text.StrTokenizer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rhq.core.domain.auth.Subject;
import org.rhq.core.domain.content.Distribution;
import org.rhq.core.domain.content.DistributionFile;
import org.rhq.core.domain.content.PackageVersion;
import org.rhq.core.domain.content.Repo;
import org.rhq.core.domain.criteria.PackageVersionCriteria;
import org.rhq.core.domain.util.PageControl;
import org.rhq.core.domain.util.PageList;
import org.rhq.enterprise.server.content.ContentManagerLocal;
import org.rhq.enterprise.server.content.ContentSourceManagerLocal;
import org.rhq.enterprise.server.content.DistributionManagerLocal;
import org.rhq.enterprise.server.content.RepoManagerLocal;
import org.rhq.enterprise.server.util.LookupUtil;
public class ContentHTTPServlet extends DefaultServlet {
/**
*
*/
private static final long serialVersionUID = 1L;
private final Log log = LogFactory.getLog(ContentHTTPServlet.class);
protected static final String CONTENT_URI = "/content/";
protected static final String PACKAGES = "packages";
protected static final String DISTRIBUTIONS = "distributions";
protected static final String REPODATA = "repodata";
protected RepoManagerLocal repoMgr;
protected ContentManagerLocal contentMgr;
protected ContentSourceManagerLocal contentSourceMgr;
protected DistributionManagerLocal distroMgr;
protected URLCodec urlCodec;
public ContentHTTPServlet() {
super();
}
public void init() throws ServletException {
super.init();
urlCodec = new URLCodec();
repoMgr = LookupUtil.getRepoManagerLocal();
contentMgr = LookupUtil.getContentManager();
contentSourceMgr = LookupUtil.getContentSourceManager();
distroMgr = LookupUtil.getDistributionManagerLocal();
}
protected boolean isIconRequest(HttpServletRequest request) {
String dir = getNthPiece(2, request.getRequestURI());
return StringUtils.equalsIgnoreCase(dir, "icons");
}
protected void doHead(HttpServletRequest request, HttpServletResponse response) throws IOException,
ServletException {
log.info("HEAD received: " + request.getRequestURI());
doGet(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException,
ServletException {
log.info("POST received: " + request.getRequestURI());
doGet(request, response);
}
protected void doPut(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
log.info("PUT received: " + request.getRequestURI());
renderErrorPage(request, response);
}
protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws IOException,
ServletException {
log.info("DELETE received: " + request.getRequestURI());
renderErrorPage(request, response);
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
log.info("doGet(): requestURI = " + request.getRequestURI());
response.setHeader("Accept-Ranges", "bytes");
if (isIconRequest(request)) {
// this request is for our icons which are static content in the webapp
// defer back to DefaultServlet.doGet and this will be served automatically
super.doGet(request, response);
return;
}
// Check if repo has been specified
Repo repo = getRepo(request, response);
if (repo == null) {
log.info("No repo found, possibly bad repo name, or no name was entered.");
renderRepoList(request, response);
return;
}
// validate entitlement
try {
ContentFilter x509Filter = new ContentFilter();
x509Filter.filter(request, repo.getId());
} catch (EntitlementException e) {
throw new ServletException(e);
}
// Check if type of content has been specified
String typeOfContent = getTypeOfContent(request.getRequestURI());
if (StringUtils.isBlank(typeOfContent)) {
log.info("no info was specified for type of content.");
renderChoiceOfContent(request, response, repo);
return;
}
//
// Determine what we should render, i.e.: packages or distributions
//
if (StringUtils.equalsIgnoreCase(typeOfContent, PACKAGES)) {
log.debug("render packages");
renderPackages(request, response, repo);
return;
} else if (StringUtils.equalsIgnoreCase(typeOfContent, DISTRIBUTIONS)) {
log.debug("render distributions");
renderDistributions(request, response, repo);
return;
} else {
log.debug("Unable to determine what type of content was requested: " + typeOfContent);
renderErrorPage(request, response);
return;
}
}
protected void renderRepoList(HttpServletRequest request, HttpServletResponse response) throws IOException {
// Two known situations
// #1 User entered a bad URL
// #2 User entered no repo name
String repoName = getRepoName(request.getRequestURI());
if (!StringUtils.isEmpty(repoName)) {
log.info("Assuming bad repo name for: " + repoName + ", will render error page");
renderErrorPage(request, response);
return;
}
// Entered repo name is blank, so we'll render a list of repos
Subject overlord = LookupUtil.getSubjectManager().getOverlord();
PageList<Repo> repos = repoMgr.findRepos(overlord, PageControl.getUnlimitedInstance());
log.debug("Returned list of repos: " + repos.getTotalSize() + " entries");
for (Repo r : repos) {
log.debug("Potential repo: Name = " + r.getName() + ", ID = " + r.getId());
}
log.debug("TODO: generate index.html");
StringBuffer sb = new StringBuffer();
HtmlRenderer.formStart(sb, "Index of ", request.getRequestURI());
for (Repo r : repos) {
log.debug("Potential repo: Name = " + r.getName() + ", ID = " + r.getId());
// Skip candidate repos
if (!r.isCandidate()) {
String lastMod = new Date(r.getLastModifiedDate()).toString();
HtmlRenderer.formDirEntry(sb, request, r.getName(), lastMod);
}
}
HtmlRenderer.formEnd(sb);
writeResponse(sb.toString(), response);
}
protected void renderChoiceOfContent(HttpServletRequest request, HttpServletResponse response, Repo repo)
throws IOException {
StringBuffer sb = new StringBuffer();
HtmlRenderer.formStart(sb, "Index of ", request.getRequestURI());
HtmlRenderer.formParentLink(sb, getParentURI(request.getRequestURI()));
HtmlRenderer.formDirEntry(sb, request, PACKAGES, "-");
HtmlRenderer.formDirEntry(sb, request, DISTRIBUTIONS, "-");
HtmlRenderer.formEnd(sb);
writeResponse(sb.toString(), response);
}
protected void renderPackages(HttpServletRequest request, HttpServletResponse response, Repo repo)
throws IOException {
log.debug("renderPackages(repo name = " + repo.getName() + ", id = " + repo.getId() + ", isCandidate = "
+ repo.isCandidate() + ")");
String fileName = getFileName(request.getRequestURI());
log.debug("Parsed file name = " + fileName);
if (StringUtils.isBlank(fileName)) {
// form a directory listing for each package in repository.
log.debug("create listing of all packages for this repo: " + repo.getName());
renderPackageIndex(request, response, repo);
return;
}
/*
* This will likely change in a future sprint.
* For now we are assuming that metadata is only yum metadata
* and it's accessed as 'packages/repodata/primary.xml'
*/
if (StringUtils.equals(fileName, REPODATA)) {
renderMetadata(request, response, repo);
return;
}
log.debug("fetch package bits and return them.");
PackageVersion pv = getPackageVersionFromFileName(repo, fileName);
if (pv == null) {
log.info("Unable to find PackageVersion from filename: " + fileName);
renderErrorPage(request, response);
return;
}
writePackageVersionBits(request, response, pv);
}
protected void renderPackageIndex(HttpServletRequest request, HttpServletResponse response, Repo repo)
throws IOException {
log.debug("Forming packages index.html for repo: " + repo.getName() + ", ID = " + repo.getId());
List<PackageVersion> pvs = repoMgr.findPackageVersionsInRepo(LookupUtil.getSubjectManager().getOverlord(), repo
.getId(), PageControl.getUnlimitedInstance());
StringBuffer sb = new StringBuffer();
HtmlRenderer.formStart(sb, "Index of ", request.getRequestURI());
HtmlRenderer.formParentLink(sb, getParentURI(request.getRequestURI()));
for (PackageVersion pv : pvs) {
HtmlRenderer.formFileEntry(sb, request, pv.getFileName(), new Date(pv.getFileCreatedDate()).toString(), pv
.getFileSize());
}
HtmlRenderer.formEnd(sb);
writeResponse(sb.toString(), response);
}
protected void renderMetadata(HttpServletRequest request, HttpServletResponse response, Repo repo)
throws IOException {
log.debug("renderMetadata(repo name = " + repo.getName() + ", id = " + repo.getId());
String metadataFilename = getMetadataFileName(request.getRequestURI());
if (StringUtils.isBlank(metadataFilename)) {
renderMetadataIndex(request, response, repo);
return;
}
// Generate Yum Metadata
log.debug("Generate Yum Metadata for : " + metadataFilename);
List<PackageVersion> pvs = repoMgr.findPackageVersionsInRepo(LookupUtil.getSubjectManager().getOverlord(), repo
.getId(), PageControl.getUnlimitedInstance());
log.debug(pvs.size() + " packages were found for repo : " + repo.getName());
StringBuffer sb = new StringBuffer();
if (!YumMetadata.generate(sb, repo, pvs, metadataFilename)) {
log.info("Error generating: " + metadataFilename + " for repo: " + repo.getName() + " with " + pvs.size()
+ " packages");
renderErrorPage(request, response);
return;
}
writeResponse(sb.toString(), response, "text/xml; charset=utf-8");
}
protected void renderMetadataIndex(HttpServletRequest request, HttpServletResponse response, Repo repo)
throws IOException {
log.debug("renderMetadataIndex(repo name = " + repo.getName() + ", id = " + repo.getId());
StringBuffer sb = new StringBuffer();
HtmlRenderer.formStart(sb, "Index of ", request.getRequestURI());
HtmlRenderer.formParentLink(sb, getParentURI(request.getRequestURI()));
// We are only supporting 2 types of yum metadata
HtmlRenderer.formFileEntry(sb, request, "primary.xml", new Date().toString(), -1);
HtmlRenderer.formFileEntry(sb, request, "repomd.xml", new Date().toString(), -1);
HtmlRenderer.formEnd(sb);
writeResponse(sb.toString(), response);
}
protected void renderDistributions(HttpServletRequest request, HttpServletResponse response, Repo repo)
throws IOException {
log.debug("renderDistributions(repo name = " + repo.getName() + ", id = " + repo.getId() + ", isCandidate = "
+ repo.isCandidate() + ")");
String distLabel = getDistLabel(request.getRequestURI());
log.debug("Parsed dist label is = " + distLabel);
if (StringUtils.isBlank(distLabel)) {
// form a directory listing for each distribution in repository.
log.info("TODO: create index.html listing all the distribution labels for this repo: " + repo.getName());
renderDistributionLabels(request, response, repo);
return;
}
// Get Distribution
Distribution dist = distroMgr.getDistributionByLabel(distLabel);
if (dist == null) {
log.info("Unable to find Distribution by label '" + distLabel + "'");
renderErrorPage(request, response);
return;
}
String fileRequest = getDistFilePath(request.getRequestURI());
if (StringUtils.isEmpty(fileRequest)) {
log.info("no distribution file was found in request, so render list of all distribution files");
renderDistributionFileList(request, response, dist);
return;
}
log.debug("Parsed DistributionFile request is for: " + fileRequest);
// Looks like a request for a distribution file
List<DistributionFile> distFiles = distroMgr.getDistributionFilesByDistId(dist.getId());
if (distFiles.isEmpty()) {
log.info("Unable to find any distribution files for dist: " + dist.getLabel());
renderErrorPage(request, response);
return;
}
for (DistributionFile dFile : distFiles) {
//log.info("Compare: " + dFile.getRelativeFilename() + " to " + fileRequest);
if (StringUtils.equalsIgnoreCase(dFile.getRelativeFilename(), fileRequest)) {
log.info("Sending back package bytes for: " + dFile.getRelativeFilename());
writeDistributionFileBits(request, response, dFile);
return;
}
}
// This isn't a DistributionFile
// This could be a request for a package. Package requests will come in as:
// ..../distributions/{Server,Cluster,Packages,etc}/a2ps-XXXXX.rpm
String possiblePkgName = getLastPiece(request.getRequestURI());
log.debug("Looking up : " + possiblePkgName + ", it might be a package request");
PackageVersion pv = getPackageVersionFromFileName(repo, possiblePkgName);
if (pv != null) {
log.info(possiblePkgName + " resolved to a package, will send package bytes back as response");
//response.setContentType("application/octet-stream");
writePackageVersionBits(request, response, pv);
return;
}
log.info("Searched through DistributionFiles and Packages, unable to find: " + fileRequest
+ ", in Distribution: " + dist.getLabel());
renderErrorPage(request, response);
}
protected void renderDistributionFileList(HttpServletRequest request, HttpServletResponse response,
Distribution dist) throws IOException {
List<DistributionFile> distFiles = distroMgr.getDistributionFilesByDistId(dist.getId());
log.debug("For Distribution label '" + dist.getLabel() + "' " + distFiles.size()
+ " distribution files were found");
StringBuffer sb = new StringBuffer();
HtmlRenderer.formStart(sb, "Index of ", request.getRequestURI());
HtmlRenderer.formParentLink(sb, getParentURI(request.getRequestURI()));
for (DistributionFile dFile : distFiles) {
HtmlRenderer.formFileEntry(sb, request, dFile.getRelativeFilename(), new Date(dFile.getLastModified())
.toString(), -1);
}
HtmlRenderer.formEnd(sb);
writeResponse(sb.toString(), response);
}
protected void renderDistributionLabels(HttpServletRequest request, HttpServletResponse response, Repo repo)
throws IOException {
StringBuffer sb = new StringBuffer();
HtmlRenderer.formStart(sb, "Index of ", request.getRequestURI());
HtmlRenderer.formParentLink(sb, getParentURI(request.getRequestURI()));
// Get list of Distributions per repo
List<Distribution> distros = repoMgr.findAssociatedDistributions(LookupUtil.getSubjectManager().getOverlord(),
repo.getId(), PageControl.getUnlimitedInstance());
log.debug("Found " + distros.size() + " for repo " + repo.getName());
for (Distribution d : distros) {
log.info("Creating link for distribution label: " + d.getLabel());
HtmlRenderer.formDirEntry(sb, request, d.getLabel(), new Date(d.getLastModifiedDate()).toString());
}
HtmlRenderer.formEnd(sb);
writeResponse(sb.toString(), response);
}
protected void renderErrorPage(HttpServletRequest request, HttpServletResponse response) throws IOException {
log.info("render error page for request: " + request.getRequestURI());
response.sendError(response.SC_NOT_FOUND);
}
protected boolean writeResponse(String data, HttpServletResponse response) throws IOException {
return writeResponse(data, response, "text/html");
}
protected boolean writeResponse(String data, HttpServletResponse response, String contentType) throws IOException {
response.setContentType(contentType);
PrintWriter out = response.getWriter();
out.write(data);
out.flush();
out.close();
return true;
}
protected Repo getRepo(HttpServletRequest request, HttpServletResponse response) {
String repoName = getRepoName(request.getRequestURI());
log.debug("Parsed repo name = " + repoName);
List<Repo> targetRepos = repoMgr.getRepoByName(repoName);
if (targetRepos.isEmpty()) {
// Check if maybe this is the repoID passed in as an int, instead of repo name
try {
Integer repoId = Integer.parseInt(repoName);
return repoMgr.getRepo(LookupUtil.getSubjectManager().getOverlord(), repoId);
} catch (NumberFormatException e) {
//ignore
}
return null;
}
return targetRepos.get(0);
}
/**
* Expecting URI format of:
* 127.0.0.1:7080/content/$REPONAME/$TYPE_OF_CONTENT/$FILENAME
*/
/**
*
* @param requestURI
* @return repo name or "" if no repo name could be determined
*/
protected String getRepoName(String requestURI) {
return getNthPiece(2, requestURI);
}
/**
*
* @param requestURI
* @return string that denotes if this is a package/distribution/etc kind of request
*/
protected String getTypeOfContent(String requestURI) {
return getNthPiece(3, requestURI);
}
/**
*
* @param requestURI
* @return file name or "" if no file name could be determined
*/
protected String getFileName(String requestURI) {
return getNthPiece(4, requestURI);
}
/**
*
* @param requestURI
* @return metadata file name or "" if no file name could be determined
*/
protected String getMetadataFileName(String requestURI) {
// expecting something like '.../packages/repodata/primary.xml'
return getNthPiece(5, requestURI);
}
protected String getDistLabel(String requestURI) {
return getNthPiece(4, requestURI);
}
protected String getLastPiece(String requestURI) {
StrTokenizer st = new StrTokenizer(decodeURL(requestURI), "/");
List<String> tokens = st.getTokenList();
if (tokens.size() < 1) {
return "";
}
return tokens.get(tokens.size() - 1);
}
/**
*
* @param requestURI
* @return
*/
protected String getDistFilePath(String requestURI) {
// Goal is we find where the distribution file path starts
// then we return the entire path, it may include an unknown
// level of sub-directories.
StrTokenizer st = new StrTokenizer(decodeURL(requestURI), "/");
List<String> tokens = st.getTokenList();
if (tokens.isEmpty()) {
return "";
}
int startIndex = 4;
String distFilePath = "";
for (int index = startIndex; index < tokens.size(); index++) {
distFilePath = distFilePath + "/" + tokens.get(index);
log.debug("index = " + index + ", distFilePath = " + distFilePath);
}
// Remove the '/' we added to the front of this string
if (distFilePath.startsWith("/")) {
distFilePath = distFilePath.substring(1);
}
return distFilePath;
}
/**
* @param n nth element to return from requestURI, (first element corresponds to 1, not 0)
* @param requestURI
*
*/
protected String getNthPiece(int n, String requestURI) {
StrTokenizer st = new StrTokenizer(decodeURL(requestURI), "/");
List<String> tokens = st.getTokenList();
if (tokens.size() < n) {
return "";
}
return tokens.get(n - 1); // caller is starting at 1 not 0
}
protected String getParentURI(String uri) {
uri = decodeURL(uri);
if (uri.endsWith("/")) {
uri = uri.substring(0, uri.length() - 1);
}
int index = uri.lastIndexOf("/");
if (index == -1) {
return uri;
}
return uri.substring(0, index);
}
protected String decodeURL(String requestURI) {
String req = null;
try {
req = urlCodec.decode(requestURI);
} catch (DecoderException e) {
log.info(e);
return "";
}
return req;
}
protected PackageVersion getPackageVersionFromFileName(Repo repo, String fileName) {
PackageVersionCriteria criteria = new PackageVersionCriteria();
criteria.addFilterFileName(fileName);
criteria.addFilterRepoId(repo.getId());
criteria.setStrict(true);
criteria.clearPaging();//disable paging as the code assumes all the results will be returned.
log.debug("Created criteria for repoId = " + repo.getId() + ", fileName = " + fileName);
List<PackageVersion> pkgVers = contentMgr.findPackageVersionsByCriteria(LookupUtil.getSubjectManager()
.getOverlord(), criteria);
for (PackageVersion pkgV : pkgVers) {
log.debug("PackageVersion found: " + pkgV);
}
log.debug("Found " + pkgVers.size() + " entries");
PackageVersion pv = null;
if (pkgVers.size() > 0) {
pv = pkgVers.get(0);
} else {
log.info("Couldn't find " + fileName + " in " + repo.getName());
}
return pv;
}
protected boolean writeDistributionFileBits(HttpServletRequest request, HttpServletResponse response,
DistributionFile distFile) throws IOException {
try {
ServletOutputStream output = response.getOutputStream();
response.setContentType("application/octet-stream");
contentSourceMgr.outputDistributionFileBits(distFile, output);
output.flush();
output.close();
} catch (IllegalStateException e) {
log.error(e);
return false;
} catch (IOException e) {
log.error(e);
return false;
}
return true;
}
protected boolean writePackageVersionBits(HttpServletRequest request, HttpServletResponse response,
PackageVersion pkgVer) {
//
// Anaconda does a fetch with specific ranges so it can locate the header in a rpm.
// Refer to anaconda source: yuminstall.py to see the details.
// We will default to range values of [0 -> -1] which will allow non-range based fetches to work as usual
//
long startRange = getStartRange(request);
long endRange = getEndRange(request);
if (endRange < 0) {
response.setContentType("application/x-rpm");
response.setContentLength(pkgVer.getFileSize().intValue());
} else {
response.setContentType("application/octet-stream");
log.debug("Range request: start = " + startRange + ", end = " + endRange);
int contentLength = new Long(endRange).intValue() - new Long(startRange).intValue() + 1;
log.debug("Setting contentLength = " + contentLength);
response.setContentLength(contentLength);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
String rangeHdr = "bytes=" + startRange + "-" + endRange + "/" + contentLength;
response.setHeader("Content-Range", rangeHdr);
}
try {
ServletOutputStream output = response.getOutputStream();
contentSourceMgr.outputPackageVersionBits(pkgVer, output, startRange, endRange);
output.flush();
output.close();
} catch (IllegalStateException e) {
log.error(e);
return false;
} catch (IOException e) {
log.error(e);
return false;
}
return true;
}
protected String[] getRanges(HttpServletRequest request) {
String rangeHeader = request.getHeader("Range");
if (rangeHeader == null) {
// Default value is 0, so it always starts at beginning
return null;
}
String[] ranges = rangeHeader.split("=");
if (ranges.length < 2) {
return null;
}
String[] indexes = ranges[1].split("-");
if (indexes.length < 2) {
return null;
}
return indexes;
}
/**
*
* @param request
* @return 0 if no value is present, or the number
*/
protected long getStartRange(HttpServletRequest request) {
String[] indexes = getRanges(request);
if ((indexes == null) || (indexes.length < 2)) {
return 0;
}
return Long.valueOf(indexes[0]).longValue();
}
/**
*
* @param request
* @return -1 or the number
*/
protected long getEndRange(HttpServletRequest request) {
String[] indexes = getRanges(request);
if ((indexes == null) || (indexes.length < 2)) {
return -1;
}
return Long.valueOf(indexes[1]).longValue();
}
}