/*
* RHQ Management Platform
* Copyright (C) 2005-2013 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.enterprise.agent;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
import mazz.i18n.Logger;
import org.rhq.core.util.MessageDigestGenerator;
import org.rhq.core.util.exception.ThrowableUtil;
import org.rhq.core.util.stream.StreamUtil;
import org.rhq.enterprise.agent.i18n.AgentI18NFactory;
import org.rhq.enterprise.agent.i18n.AgentI18NResourceKeys;
/**
* Downloads the agent update binary from the server, if one is available.
*
* @author John Mazzitelli
*/
public class AgentUpdateDownload {
private static final Logger LOG = AgentI18NFactory.getLogger(AgentUpdateDownload.class);
private final AgentMain agent;
private final AgentUpdateVersion agentUpdateVersion;
private File downloadedFile;
public AgentUpdateDownload(AgentMain agent) {
this.agent = agent;
this.agentUpdateVersion = new AgentUpdateVersion(agent);
this.downloadedFile = null;
}
public AgentUpdateVersion getAgentUpdateVersion() {
return agentUpdateVersion;
}
/**
* Returns the URL that will be accessed to download the agent update binary.
*
* @return download URL
*
* @throws Exception if for some reason a valid URL could not be obtained
*/
public URL getDownloadUrl() throws Exception {
return new URL(this.agent.getConfiguration().getAgentUpdateDownloadUrl());
}
/**
* If the agent update binary has been {@link #download() downloaded}, this will return
* the file where the downloaded content was stored.
*
* @return the file of the downloaded agent update binary; <code>null</code> if it was
* never downloaded
*/
public File getAgentUpdateBinaryFile() {
return this.downloadedFile;
}
/**
* Returns the location on the local file system where any downloaded
* files will be stored.
*
* @return local file system location where the downloaded files are stored
*/
public File getLocalDownloadDirectory() {
String agentHome = this.agent.getAgentHomeDirectory();
File dir = null;
if (agentHome != null && agentHome.length() > 0) {
dir = (new File(agentHome)).getParentFile();
}
if (dir == null) {
dir = new File(System.getProperty("java.io.tmpdir"));
}
return dir;
}
/**
* This will validate the MD5 of the {@link #getAgentUpdateBinaryFile() downloaded binary file}.
* If it validates, this method returns normally, otherwise, an exception is thrown.
* You must first download the file first before calling this method.
*
* @throws Exception if the downloaded agent update binary file does not validate with the expected
* MD5, or the agent update binary has not been downloaded yet.
*/
public void validate() throws Exception {
File fileToValidate = this.downloadedFile;
if (fileToValidate == null || !fileToValidate.exists()) {
throw new IllegalStateException(this.agent.getI18NMsg().getMsg(
AgentI18NResourceKeys.UPDATE_DOWNLOAD_MD5_MISSING_FILE, fileToValidate));
}
AgentUpdateInformation info = this.agentUpdateVersion.getAgentUpdateInformation();
String md5 = info.getUpdateMd5();
if (!validateFile(this.downloadedFile, md5)) {
// its invalid, move it out of the way so we can download a new one
File invalidFile = new File(fileToValidate.getParentFile(), fileToValidate.getName() + ".invalid");
invalidFile.delete(); // remove any old one that might be hanging around
fileToValidate.renameTo(invalidFile);
throw new IllegalStateException(this.agent.getI18NMsg().getMsg(
AgentI18NResourceKeys.UPDATE_DOWNLOAD_MD5_INVALID, fileToValidate));
}
return; // it validates OK
}
/**
* Downloads the agent update binary and stores it to the local file system.
*
* @throws Exception if agent has disabled updates or it failed to download the update
*/
public void download() throws Exception {
if (!agent.getConfiguration().isAgentUpdateEnabled()) {
throw new Exception(this.agent.getI18NMsg().getMsg(AgentI18NResourceKeys.UPDATE_DOWNLOAD_DISABLED_BY_AGENT));
}
// if need be, asks the server for the info - let this throw its own exceptions if it needs to
AgentUpdateInformation info = this.agentUpdateVersion.getAgentUpdateInformation();
URL url = null;
boolean keep_going = true;
File binaryFile = null;
while (keep_going) {
HttpURLConnection conn = null;
InputStream inStream = null;
try {
// we only support http/s
url = getDownloadUrl();
LOG.info(AgentI18NResourceKeys.UPDATE_DOWNLOAD_RETRIEVAL, info, url);
if (url.getProtocol().equals("https")) {
conn = openSecureConnection(url);
} else {
conn = (HttpURLConnection) url.openConnection(); // we only support http(s), so this cast is OK
}
inStream = conn.getInputStream();
// put the update content in the local file system
// determine what the name should be of the agent update binary based on the header
// Content-Disposition: attachment; filename=<filename.is.here>
String fileName = conn.getHeaderField("Content-Disposition");
if (fileName != null) {
int filenameIndex = fileName.indexOf("filename=");
if (filenameIndex > -1 && (filenameIndex + "filename=".length()) < fileName.length()) {
fileName = fileName.substring(filenameIndex + "filename=".length()).trim();
} else {
LOG.warn(AgentI18NResourceKeys.UPDATE_DOWNLOAD_BAD_NAME, fileName);
fileName = null;
}
}
if (fileName == null || fileName.length() == 0) {
// this should never happen, server should always give us the content-disposition
// but just in case the download URL is not pointing to a RHQ server and the URL it
// is pointing to doesn't give us that header, make a best guess at the name
fileName = "rhq-enterprise-agent-" + info.getUpdateVersion() + ".jar";
LOG.info(AgentI18NResourceKeys.UPDATE_DOWNLOAD_NO_NAME, fileName);
}
File dir = getLocalDownloadDirectory();
binaryFile = new File(dir, fileName);
// don't bother downloading if we already have it!
if (validateFile(binaryFile, info.getUpdateMd5())) {
LOG.debug(AgentI18NResourceKeys.UPDATE_DOWNLOAD_ALREADY_HAVE_IT, binaryFile);
// we end up closing the stream before reading the data which causes an ugly error in the server log
// should we consider doing a HTTP HEAD request first?
keep_going = false;
break;
}
// slurp the entire agent update binary content and store it to our file
binaryFile.delete();
FileOutputStream fos = new FileOutputStream(binaryFile);
StreamUtil.copy(inStream, fos, true);
inStream = null;
keep_going = false;
} catch (Exception e) {
if (conn != null) {
int responseCode = 0;
try {
responseCode = conn.getResponseCode();
} catch (Exception ignore) {
}
if (responseCode == HttpURLConnection.HTTP_UNAVAILABLE) {
// server is overloaded with other agents downloading, we must wait
LOG.info(AgentI18NResourceKeys.UPDATE_DOWNLOAD_UNAVAILABLE, info, url);
Thread.sleep(getRetryAfter(conn)); // sleep alittle bit to give the server some time (allow us to be interrupted!)
keep_going = true;
} else if (responseCode == HttpURLConnection.HTTP_FORBIDDEN) {
// server has disabled agent updates
Exception e1 = new Exception(this.agent.getI18NMsg().getMsg(
AgentI18NResourceKeys.UPDATE_DOWNLOAD_DISABLED_BY_SERVER, url), e);
LOG.warn(AgentI18NResourceKeys.UPDATE_DOWNLOAD_FAILURE, url, ThrowableUtil.getAllMessages(e1));
throw e1;
} else {
// some unexpected error occurred
LOG.warn(AgentI18NResourceKeys.UPDATE_DOWNLOAD_FAILURE, url, ThrowableUtil.getAllMessages(e));
throw e;
}
} else {
LOG.warn(AgentI18NResourceKeys.UPDATE_DOWNLOAD_FAILURE, url, ThrowableUtil.getAllMessages(e));
throw e;
}
} finally {
if (inStream != null) {
try {
inStream.close();
} catch (Exception e) {
}
}
if (conn != null) {
try {
conn.disconnect();
} catch (Exception e) {
}
}
}
}
// we only ever get here when we are successful
LOG.info(AgentI18NResourceKeys.UPDATE_DOWNLOAD_DONE, info, url, binaryFile);
this.downloadedFile = binaryFile;
return;
}
/**
* Gets the "Retry-After" header and returns its value as a long to indicate how long
* we should wait before retrying. If can't get the header, a default time interval will be returned.
*
* @param conn the connection where the header can be found
*
* @return the header value, as a long
*/
private long getRetryAfter(HttpURLConnection conn) {
try {
// get the header - by spec, it must be in seconds
int retryAfter = conn.getHeaderFieldInt("Retry-After", 30);
return 1000L * retryAfter;
} catch (Exception e) {
return 30000L;
}
}
/**
* This will validate the MD5 of the given file.
*
* @return <code>true</code> if the file's MD5 matches the given MD5, <code>false</code> otherwise
*/
private boolean validateFile(File file, String md5) {
try {
String filemd5 = MessageDigestGenerator.getDigestString(file);
return (filemd5 != null && filemd5.equals(md5));
} catch (Exception e) {
return false;
}
}
private HttpsURLConnection openSecureConnection(URL url) throws Exception {
AgentConfiguration config = this.agent.getConfiguration();
SecureConnectorFactory secureConnectorFactory = new SecureConnectorFactory();
SecureConnector secureConnector = secureConnectorFactory.getInstanceWithAgentConfiguration(
config, this.agent.getAgentHomeDirectory());
return secureConnector.openSecureConnection(url);
}
}