/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! Inc., Seiji Sogabe
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.model;
import hudson.BulkChange;
import hudson.Extension;
import hudson.ExtensionPoint;
import hudson.Functions;
import hudson.PluginManager;
import hudson.PluginWrapper;
import hudson.ProxyConfiguration;
import hudson.security.ACLContext;
import jenkins.util.SystemProperties;
import hudson.Util;
import hudson.XmlFile;
import static hudson.init.InitMilestone.PLUGINS_STARTED;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import hudson.init.Initializer;
import hudson.lifecycle.Lifecycle;
import hudson.lifecycle.RestartNotSupportedException;
import hudson.model.UpdateSite.Data;
import hudson.model.UpdateSite.Plugin;
import hudson.model.listeners.SaveableListener;
import hudson.remoting.AtmostOneThreadExecutor;
import hudson.security.ACL;
import hudson.util.DaemonThreadFactory;
import hudson.util.FormValidation;
import hudson.util.HttpResponses;
import hudson.util.NamingThreadFactory;
import hudson.util.IOException2;
import hudson.util.IOUtils;
import hudson.util.PersistedList;
import hudson.util.XStream2;
import jenkins.MissingDependencyException;
import jenkins.RestartRequiredException;
import jenkins.install.InstallUtil;
import jenkins.model.Jenkins;
import jenkins.util.io.OnMaster;
import net.sf.json.JSONObject;
import org.acegisecurity.Authentication;
import org.acegisecurity.context.SecurityContext;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.input.CountingInputStream;
import org.apache.commons.io.output.NullOutputStream;
import org.jenkinsci.Symbol;
import org.jvnet.localizer.Localizable;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.annotation.Nonnull;
import javax.net.ssl.SSLHandshakeException;
import javax.servlet.ServletException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.net.HttpRetryException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import org.acegisecurity.context.SecurityContextHolder;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.interceptor.RequirePOST;
/**
* Controls update center capability.
*
*
* The main job of this class is to keep track of the latest update center metadata file, and perform installations.
* Much of the UI about choosing plugins to install is done in {@link PluginManager}.
*
* The update center can be configured to contact alternate servers for updates
* and plugins, and to use alternate strategies for downloading, installing
* and updating components. See the Javadocs for {@link UpdateCenterConfiguration}
* for more information.
*
* Extending Update Centers. The update center in {@code Jenkins} can be replaced by defining a
* System Property (hudson.model.UpdateCenter.className
). See {@link #createUpdateCenter(hudson.model.UpdateCenter.UpdateCenterConfiguration)}.
* This className should be available on early startup, so it cannot come only from a library
* (e.g. Jenkins module or Extra library dependency in the WAR file project).
* Plugins cannot be used for such purpose.
* In order to be correctly instantiated, the class definition must have two constructors:
* {@link #UpdateCenter()} and {@link #UpdateCenter(hudson.model.UpdateCenter.UpdateCenterConfiguration)}.
* If the class does not comply with the requirements, a fallback to the default UpdateCenter will be performed.
*
* @author Kohsuke Kawaguchi
* @since 1.220
*/
@ExportedBean
public class UpdateCenter extends AbstractModelObject implements Saveable, OnMaster {
private static final String UPDATE_CENTER_URL = SystemProperties.getString(UpdateCenter.class.getName()+".updateCenterUrl","http://updates.jenkins-ci.org/");
/**
* Read timeout when downloading plugins, defaults to 1 minute
*/
private static final int PLUGIN_DOWNLOAD_READ_TIMEOUT = SystemProperties.getInteger(UpdateCenter.class.getName()+".pluginDownloadReadTimeoutSeconds", 60) * 1000;
public static final String PREDEFINED_UPDATE_SITE_ID = "default";
/**
* {@linkplain UpdateSite#getId() ID} of the default update site.
* @since 1.483; configurable via system property since 2.4
*/
public static final String ID_DEFAULT = SystemProperties.getString(UpdateCenter.class.getName()+".defaultUpdateSiteId", PREDEFINED_UPDATE_SITE_ID);
@Restricted(NoExternalUse.class)
public static final String ID_UPLOAD = "_upload";
/**
* {@link ExecutorService} that performs installation.
* @since 1.501
*/
private final ExecutorService installerService = new AtmostOneThreadExecutor(
new NamingThreadFactory(new DaemonThreadFactory(), "Update center installer thread"));
/**
* An {@link ExecutorService} for updating UpdateSites.
*/
protected final ExecutorService updateService = Executors.newCachedThreadPool(
new NamingThreadFactory(new DaemonThreadFactory(), "Update site data downloader"));
/**
* List of created {@link UpdateCenterJob}s. Access needs to be synchronized.
*/
private final Vector jobs = new Vector();
/**
* {@link UpdateSite}s from which we've already installed a plugin at least once.
* This is used to skip network tests.
*/
private final Set sourcesUsed = new HashSet();
/**
* List of {@link UpdateSite}s to be used.
*/
private final PersistedList sites = new PersistedList(this);
/**
* Update center configuration data
*/
private UpdateCenterConfiguration config;
private boolean requiresRestart;
/**
* Simple connection status enum.
*/
@Restricted(NoExternalUse.class)
static enum ConnectionStatus {
/**
* Connection status has not started yet.
*/
PRECHECK,
/**
* Connection status check has been skipped.
* As example, it may happen if there is no connection check URL defined for the site.
* @since 2.4
*/
SKIPPED,
/**
* Connection status is being checked at this time.
*/
CHECKING,
/**
* Connection status was not checked.
*/
UNCHECKED,
/**
* Connection is ok.
*/
OK,
/**
* Connection status check failed.
*/
FAILED;
static final String INTERNET = "internet";
static final String UPDATE_SITE = "updatesite";
}
public UpdateCenter() {
configure(new UpdateCenterConfiguration());
}
UpdateCenter(@Nonnull UpdateCenterConfiguration configuration) {
configure(configuration);
}
/**
* Creates an update center.
* @param config Requested configuration. May be {@code null} if defaults should be used
* @return Created Update center. {@link UpdateCenter} by default, but may be overridden
* @since 2.4
*/
@Nonnull
public static UpdateCenter createUpdateCenter(@CheckForNull UpdateCenterConfiguration config) {
String requiredClassName = SystemProperties.getString(UpdateCenter.class.getName()+".className", null);
if (requiredClassName == null) {
// Use the defaul Update Center
LOGGER.log(Level.FINE, "Using the default Update Center implementation");
return createDefaultUpdateCenter(config);
}
LOGGER.log(Level.FINE, "Using the custom update center: {0}", requiredClassName);
try {
final Class> clazz = Class.forName(requiredClassName).asSubclass(UpdateCenter.class);
if (!UpdateCenter.class.isAssignableFrom(clazz)) {
LOGGER.log(Level.SEVERE, "The specified custom Update Center {0} is not an instance of {1}. Falling back to default.",
new Object[] {requiredClassName, UpdateCenter.class.getName()});
return createDefaultUpdateCenter(config);
}
final Class extends UpdateCenter> ucClazz = clazz.asSubclass(UpdateCenter.class);
final Constructor extends UpdateCenter> defaultConstructor = ucClazz.getConstructor();
final Constructor extends UpdateCenter> configConstructor = ucClazz.getConstructor(UpdateCenterConfiguration.class);
LOGGER.log(Level.FINE, "Using the constructor {0} Update Center configuration for {1}",
new Object[] {config != null ? "with" : "without", requiredClassName});
return config != null ? configConstructor.newInstance(config) : defaultConstructor.newInstance();
} catch(ClassCastException e) {
// Should never happen
LOGGER.log(WARNING, "UpdateCenter class {0} does not extend hudson.model.UpdateCenter. Using default.", requiredClassName);
} catch(NoSuchMethodException e) {
LOGGER.log(WARNING, String.format("UpdateCenter class %s does not define one of the required constructors. Using default", requiredClassName), e);
} catch(Exception e) {
LOGGER.log(WARNING, String.format("Unable to instantiate custom plugin manager [%s]. Using default.", requiredClassName), e);
}
return createDefaultUpdateCenter(config);
}
@Nonnull
private static UpdateCenter createDefaultUpdateCenter(@CheckForNull UpdateCenterConfiguration config) {
return config != null ? new UpdateCenter(config) : new UpdateCenter();
}
public Api getApi() {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
return new Api(this);
}
/**
* Configures update center to get plugins/updates from alternate servers,
* and optionally using alternate strategies for downloading, installing
* and upgrading.
*
* @param config Configuration data
* @see UpdateCenterConfiguration
*/
public void configure(UpdateCenterConfiguration config) {
if (config!=null) {
this.config = config;
}
}
/**
* Returns the list of {@link UpdateCenterJob} representing scheduled installation attempts.
*
* @return
* can be empty but never null. Oldest entries first.
*/
@Exported
public List getJobs() {
synchronized (jobs) {
return new ArrayList(jobs);
}
}
/**
* Gets a job by its ID.
*
* Primarily to make {@link UpdateCenterJob} bound to URL.
*/
public UpdateCenterJob getJob(int id) {
synchronized (jobs) {
for (UpdateCenterJob job : jobs) {
if (job.id==id)
return job;
}
}
return null;
}
/**
* Returns latest install/upgrade job for the given plugin.
* @return InstallationJob or null if not found
*/
public InstallationJob getJob(Plugin plugin) {
List jobList = getJobs();
Collections.reverse(jobList);
for (UpdateCenterJob job : jobList)
if (job instanceof InstallationJob) {
InstallationJob ij = (InstallationJob)job;
if (ij.plugin.name.equals(plugin.name) && ij.plugin.sourceId.equals(plugin.sourceId))
return ij;
}
return null;
}
/**
* Get the current connection status.
*
* Supports a "siteId" request parameter, defaulting to {@link #ID_DEFAULT} for the default
* update site.
*
* @return The current connection status.
*/
@Restricted(DoNotUse.class)
public HttpResponse doConnectionStatus(StaplerRequest request) {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
try {
String siteId = request.getParameter("siteId");
if (siteId == null) {
siteId = ID_DEFAULT;
} else if (siteId.equals("default")) {
// If the request explicitly requires the default ID, ship it
siteId = ID_DEFAULT;
}
ConnectionCheckJob checkJob = getConnectionCheckJob(siteId);
if (checkJob == null) {
UpdateSite site = getSite(siteId);
if (site != null) {
checkJob = addConnectionCheckJob(site);
}
}
if (checkJob != null) {
boolean isOffline = false;
for (ConnectionStatus status : checkJob.connectionStates.values()) {
if(ConnectionStatus.FAILED.equals(status)) {
isOffline = true;
break;
}
}
if (isOffline) {
// retry connection states if determined to be offline
checkJob.run();
isOffline = false;
for (ConnectionStatus status : checkJob.connectionStates.values()) {
if(ConnectionStatus.FAILED.equals(status)) {
isOffline = true;
break;
}
}
if(!isOffline) { // also need to download the metadata
updateAllSites();
}
}
return HttpResponses.okJSON(checkJob.connectionStates);
} else {
return HttpResponses.errorJSON(String.format("Cannot check connection status of the update site with ID='%s'"
+ ". This update center cannot be resolved", siteId));
}
} catch (Exception e) {
return HttpResponses.errorJSON(String.format("ERROR: %s", e.getMessage()));
}
}
/**
* Called to determine if there was an incomplete installation, what the statuses of the plugins are
*/
@Restricted(DoNotUse.class) // WebOnly
public HttpResponse doIncompleteInstallStatus() {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
try {
Map jobs = InstallUtil.getPersistedInstallStatus();
if(jobs == null) {
jobs = Collections.emptyMap();
}
return HttpResponses.okJSON(jobs);
} catch (Exception e) {
return HttpResponses.errorJSON(String.format("ERROR: %s", e.getMessage()));
}
}
/**
* Called to persist the currently installing plugin states. This allows
* us to support install resume if Jenkins is restarted while plugins are
* being installed.
*/
@Restricted(NoExternalUse.class)
public synchronized void persistInstallStatus() {
List jobs = getJobs();
boolean activeInstalls = false;
for (UpdateCenterJob job : jobs) {
if (job instanceof InstallationJob) {
InstallationJob installationJob = (InstallationJob) job;
if(!installationJob.status.isSuccess()) {
activeInstalls = true;
}
}
}
if(activeInstalls) {
InstallUtil.persistInstallStatus(jobs); // save this info
}
else {
InstallUtil.clearInstallStatus(); // clear this info
}
}
/**
* Get the current installation status of a plugin set.
*
* Supports a "correlationId" request parameter if you only want to get the
* install status of a set of plugins requested for install through
* {@link PluginManager#doInstallPlugins(org.kohsuke.stapler.StaplerRequest)}.
*
* @return The current installation status of a plugin set.
*/
@Restricted(DoNotUse.class)
public HttpResponse doInstallStatus(StaplerRequest request) {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
try {
String correlationId = request.getParameter("correlationId");
Map response = new HashMap<>();
response.put("state", Jenkins.getInstance().getInstallState().name());
List