/*
* Copyright (c) 2013, CloudBees, Inc., SOASTA, Inc.
* All Rights Reserved.
*/
package com.soasta.jenkins;
import java.io.IOException;
import java.net.URL;
import hudson.FilePath;
import hudson.model.Node;
import hudson.model.TaskListener;
import hudson.tools.DownloadFromUrlInstaller;
import hudson.tools.ToolInstallation;
import hudson.util.VersionNumber;
/*******************************************************************************************************************************
* START (Jenkins User-Agent change related imports)
*******************************************************************************************************************************/
import hudson.Functions;
import hudson.ProxyConfiguration;
import hudson.FilePath.FileCallable;
import hudson.remoting.VirtualChannel;
import hudson.util.IOUtils;
import java.io.File;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URLConnection;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import jenkins.model.Jenkins;
import org.apache.commons.io.input.CountingInputStream;
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipFile;
import org.jenkinsci.remoting.RoleChecker;
import com.soasta.jenkins.httpclient.GenericSelfClosingHttpClient;
import com.soasta.jenkins.httpclient.HttpClientSettings;
/*******************************************************************************************************************************
* END (Jenkins User-Agent change related imports)
*******************************************************************************************************************************/
public class CommonInstaller extends DownloadFromUrlInstaller
{
private final CloudTestServer server;
private final VersionNumber buildNumber;
private final Installers installerType;
/* Uncomment to test on localhost */
/*
static {
//for localhost testing only
javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier(
new javax.net.ssl.HostnameVerifier(){
public boolean verify(String hostname,
javax.net.ssl.SSLSession sslSession) {
if (hostname.equals("localhost")) {
return true;
}
return false;
}
});
} */
private CommonInstaller(CloudTestServer server, Installers installerType, VersionNumber buildNumber) {
super(installerType.getCTInstallerType()+buildNumber);
this.server = server;
this.installerType = installerType;
this.buildNumber = buildNumber;
}
CommonInstaller(CloudTestServer server, Installers installFileType) throws IOException {
this(server, installFileType, server.getBuildNumber());
}
CloudTestServer getServer() {
return server;
}
VersionNumber getBuildNumber() {
return buildNumber;
}
Installers getInstallerType() {
return installerType;
}
/*******************************************************************************************************************************
* START (Jenkins User-Agent related code changes)
*******************************************************************************************************************************/
/**
* The following code changes the file download request for installer files
* (iOSAppInstaller, MATT, or SCommand) to have a User-Agent of
* "Jenkins/<Jenkins version #>" instead of "Java/<Java version #>".
* This allow this call to correctly identify itself as coming from the Jenkins
* plugin.
*
* WARNING: This code was taken from Jenkins (1.544) src, FilePath.java
* and DownloadFromUrlInstaller.java.
* A few changes were made to logging and to make the code work. May be
* vulnerable to Jenkins base code changes in the future. Issues related
* to this may crop up in the future concerning any type of installer
* downloading because of this. This addition is because of Bug 71924.
*
* TODO: Remove code when Jenkins will properly identify itself in the
* User-Agent. All the code below is tied to being able to add to the
* URLConnection that the User-Agent is, instead, "Jenkins/..." and not
* "Java/...".
*/
public FilePath performInstallation(ToolInstallation tool, Node node, TaskListener log) throws IOException, InterruptedException {
try
{
return super.performInstallation(tool, node, log);
}
catch (IOException e)
{
FilePath expected = preferredLocation(tool, node);
Installable inst = getInstallable();
if(installIfNecessaryFrom(expected, new URL(inst.url), log, "Unpacking " + inst.url + " to " + expected + " on " + node.getDisplayName())) {
expected.child(".timestamp").delete(); // we don't use the timestamp
FilePath base = findPullUpDirectory(expected);
if(base!=null && base!=expected)
base.moveAllChildrenTo(expected);
// leave a record for the next up-to-date check
expected.child(".installedFrom").write(inst.url,"UTF-8");
expected.act(new ChmodRecAPlusX());
}
return expected;
}
}
/**
* Sets execute permission on all files, since unzip etc. might not do this.
* Hackish, is there a better way?
*/
static class ChmodRecAPlusX implements FileCallable<Void> {
private static final long serialVersionUID = 1L;
public Void invoke(File d, VirtualChannel channel) throws IOException {
if(!Functions.isWindows())
process(d);
return null;
}
private void process(File f) {
if (f.isFile()) {
f.setExecutable(true, false);
} else {
File[] kids = f.listFiles();
if (kids != null) {
for (File kid : kids) {
process(kid);
}
}
}
}
@Override
public void checkRoles(RoleChecker arg0) throws SecurityException
{
// TODO Auto-generated method stub
}
}
/**
* (From FilePath.java)
* Given a zip file, extracts it to the given target directory, if necessary.
*
* <p>
* This method is a convenience method designed for installing a binary package to a location
* that supports upgrade and downgrade. Specifically,
*
* <ul>
* <li>If the target directory doesn't exist {@linkplain #mkdirs() it'll be created}.
* <li>If the timestamp left in the directory doesn't match with the timestamp of the current archive file,
* the directory contents will be discarded and the archive file will be re-extracted.
* <li>If the connection is refused but the target directory already exists, it is left alone.
* </ul>
*
* @param archive
* The resource that represents the zip file. This URL must support the "Last-Modified" header.
* (Most common usage is to get this from {@link ClassLoader#getResource(String)})
* @param listener
* If non-null, a message will be printed to this listener once this method decides to
* extract an archive.
* @return
* true if the archive was extracted. false if the extraction was skipped because the target directory
* was considered up to date.
* @since 1.299
*/
private boolean installIfNecessaryFrom(FilePath expected, URL archive, TaskListener listener, String message) throws IOException, InterruptedException {
try {
FilePath timestamp = expected.child(".timestamp");
URLConnection con;
try {
con = ProxyConfiguration.open(archive);
// Jira Bug JENKINS-21033: Changing the User-Agent from "Java/<Java version #>" to "Jenkins/<Jenkins version #>"
con.setRequestProperty("User-Agent", "Jenkins/" + Jenkins.getVersion().toString());
// requested connection is HTTPS.
if (con instanceof HttpsURLConnection)
{
HttpsURLConnection https = ((HttpsURLConnection) con);
CloudTestServer server = getServer();
String password = server.getKeyStorePassword().getPlainText() == null || server.getKeyStorePassword().getPlainText().isEmpty() ? null : server.getKeyStorePassword().getPlainText();
HttpClientSettings settings = new HttpClientSettings()
.setKeyStore(HttpClientSettings.loadKeyStore(server.getKeyStoreLocation(), password))
.setKeyStorePassword(server.getKeyStorePassword().getPlainText())
.setTrustSelfSigned(server.isTrustSelfSigned());
KeyManager[] keyManagers = GenericSelfClosingHttpClient.getKeyManagers(settings);
TrustManager[] trustManagers = null;
if (server.isTrustSelfSigned())
{
trustManagers = GenericSelfClosingHttpClient.getTrustAllSelfSigned();
}
SSLContext sslContext = null;
try
{
sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, new java.security.SecureRandom());
https.setSSLSocketFactory(sslContext.getSocketFactory());
}
catch (Exception e)
{
LOGGER.log(Level.SEVERE, "Error setting ssl context", e);
}
}
LOGGER.log(Level.INFO, "Setting User-Agent for download to " + con.getRequestProperty("User-Agent") +
" for file " + archive.getPath());
if (timestamp.exists()) {
con.setIfModifiedSince(timestamp.lastModified());
}
con.connect();
} catch (IOException x) {
if (expected.exists()) {
// Cannot connect now, so assume whatever was last unpacked is still OK.
if (listener != null) {
LOGGER.log(Level.INFO, "Skipping installation of " + archive + " to " + expected.getRemote() + ": " + x);
}
return false;
} else {
throw x;
}
}
LOGGER.log(Level.INFO, "Connection response code is: " + ((HttpURLConnection)con).getResponseCode());
if (con instanceof HttpURLConnection
&& ((HttpURLConnection)con).getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
return false;
}
long sourceTimestamp = con.getLastModified();
if(expected.exists()) {
LOGGER.log(Level.INFO, "Not creating a new " + expected.getRemote() + " as one already exists.");
if(timestamp.exists() && sourceTimestamp ==timestamp.lastModified())
return false; // already up to date
expected.deleteContents();
} else {
expected.mkdirs();
}
if(listener!=null)
LOGGER.log(Level.INFO, message);
if (expected.isRemote()) {
LOGGER.log(Level.INFO, "Treating this as a remote request.");
// First try to download from the slave machine.
try {
expected.act(new Unpack(archive));
timestamp.touch(sourceTimestamp);
return true;
} catch (IOException x) {
if (listener != null) {
listener.error("Failed to download " + archive + " because of " + x.getLocalizedMessage() + "; will retry.");
}
}
}
expected.act(new Unpack(archive));
timestamp.touch(sourceTimestamp);
return true;
} catch (IOException e) {
throw new IOException("Failed to install "+archive+" to "+ expected.getRemote(),e);
}
}
private static final class Unpack implements FileCallable<Void> {
private static final long serialVersionUID = 1L;
private final URL archive;
Unpack(URL archive) {
this.archive = archive;
}
public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
URLConnection con = archive.openConnection();
// Jira Bug JENKINS-21033: Changing the User-Agent from "Java/<Java version #>" to "Jenkins/<Jenkins version #>"
con.setRequestProperty("User-Agent", "Jenkins/" + Jenkins.getVersion().toString());
InputStream in = con.getInputStream();
try {
CountingInputStream cis = new CountingInputStream(in);
try {
LOGGER.log(Level.INFO, "Invoke called for Unpack class to unpack to " + dir.getAbsolutePath());
if (archive.toExternalForm().endsWith(".zip")) {
LOGGER.log(Level.INFO, "Archive unzipped as it ends with '.zip'. Starting unzip.");
unzip(dir, cis);
}
} catch (IOException x) {
throw new IOException(String.format("Failed to unpack %s (%d bytes read)", archive, cis.getByteCount()), x);
}
} finally {
in.close();
}
return null;
}
private static void unzip(File dir, InputStream in) throws IOException {
File tmpFile = File.createTempFile("tmpzip", null); // uses java.io.tmpdir
try {
IOUtils.copy(in, tmpFile);
unzip(dir,tmpFile);
}
finally {
tmpFile.delete();
}
}
static private void unzip(File dir, File zipFile) throws IOException {
dir = dir.getAbsoluteFile(); // without absolutization, getParentFile below seems to fail
ZipFile zip = new ZipFile(zipFile);
@SuppressWarnings("unchecked")
Enumeration<ZipEntry> entries = zip.getEntries();
try {
while (entries.hasMoreElements()) {
ZipEntry e = entries.nextElement();
File f = new File(dir, e.getName());
if (e.isDirectory()) {
f.mkdirs();
} else {
File p = f.getParentFile();
if (p != null) {
p.mkdirs();
}
InputStream input = zip.getInputStream(e);
try {
IOUtils.copy(input, f);
} finally {
input.close();
}
try {
FilePath target = new FilePath(f);
int mode = e.getUnixMode();
if (mode!=0) // Ant returns 0 if the archive doesn't record the access mode
target.chmod(mode);
} catch (InterruptedException ex) {
LOGGER.log(Level.WARNING, "unable to set permissions", ex);
}
f.setLastModified(e.getTime());
}
}
} finally {
zip.close();
}
}
@Override
public void checkRoles(RoleChecker arg0) throws SecurityException
{
// TODO Auto-generated method stub
}
}
/*******************************************************************************************************************************
* END (Jenkins User-Agent related code changes)
*******************************************************************************************************************************/
private final static Logger LOGGER = Logger.getLogger(CommonInstaller.class.getName());
}