diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index 60b23cca7fb7b4d46c5befddaed593127e326d15..a5bbc68493b3282f2361d13e50c5844c07df048f 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -1067,13 +1067,6 @@ public class Functions { return null; } - /** - * Gets the URL for the update center server - */ - public String getUpdateCenterUrl() { - return Hudson.getInstance().getUpdateCenter().getUrl(); - } - /** * If the given href link is matching the current page, return true. * diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index d4cbe81e6fbd58b0d8c0e05a918b0ce2b084262a..9086bc8501a49da8d6bd8e294a3e41c24ca6161b 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -26,6 +26,7 @@ package hudson; import hudson.model.AbstractModelObject; import hudson.model.Failure; import hudson.model.Hudson; +import hudson.model.UpdateSite; import hudson.model.UpdateCenter; import hudson.util.Service; import org.apache.commons.fileupload.FileItem; @@ -352,6 +353,25 @@ public final class PluginManager extends AbstractModelObject { LogFactory.release(uberClassLoader); } + public HttpResponse doUpdateSources(StaplerRequest req) throws IOException { + Hudson.getInstance().checkPermission(Hudson.ADMINISTER); + + if (req.hasParameter("remove")) { + UpdateCenter uc = Hudson.getInstance().getUpdateCenter(); + BulkChange bc = new BulkChange(uc); + try { + for (String id : req.getParameterValues("sources")) + uc.getSites().remove(uc.getById(id)); + } finally { + bc.commit(); + } + } else + if (req.hasParameter("add")) + return new HttpRedirect("addSite"); + + return new HttpRedirect("./sites"); + } + /** * Performs the installation of the plugins. */ @@ -361,10 +381,13 @@ public final class PluginManager extends AbstractModelObject { String n = en.nextElement(); if(n.startsWith("plugin.")) { n = n.substring(7); - UpdateCenter.Plugin p = Hudson.getInstance().getUpdateCenter().getPlugin(n); - if(p==null) - throw new Failure("No such plugin: "+n); - p.deploy(); + if (n.indexOf(".") > 0) { + String[] pluginInfo = n.split("\\."); + UpdateSite.Plugin p = Hudson.getInstance().getUpdateCenter().getById(pluginInfo[1]).getPlugin(pluginInfo[0]); + if(p==null) + throw new Failure("No such plugin: "+n); + p.deploy(); + } } } rsp.sendRedirect("../updateCenter/"); @@ -376,9 +399,9 @@ public final class PluginManager extends AbstractModelObject { @QueryParameter("proxy.userName") String userName, @QueryParameter("proxy.password") String password, StaplerResponse rsp) throws IOException { - Hudson.getInstance().checkPermission(Hudson.ADMINISTER); - Hudson hudson = Hudson.getInstance(); + hudson.checkPermission(Hudson.ADMINISTER); + server = Util.fixEmptyAndTrim(server); if(server==null) { hudson.proxy = null; diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index 4a9c9a81191e57976efa3394754598b56b5997ac..4f610a0910439ee2c0b2475c0c8f5ab26ee56c8f 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -25,6 +25,7 @@ package hudson; import hudson.model.Hudson; import hudson.model.UpdateCenter; +import hudson.model.UpdateSite; import java.io.File; import java.io.FileOutputStream; @@ -252,7 +253,7 @@ public final class PluginWrapper { if(url!=null) return url; // fallback to update center metadata - UpdateCenter.Plugin ui = getInfo(); + UpdateSite.Plugin ui = getInfo(); if(ui!=null) return ui.wiki; return null; @@ -360,23 +361,23 @@ public final class PluginWrapper { /** * If the plugin has {@link #getUpdateInfo() an update}, - * returns the {@link UpdateCenter.Plugin} object. + * returns the {@link UpdateSite.Plugin} object. * * @return * This method may return null — for example, * the user may have installed a plugin locally developed. */ - public UpdateCenter.Plugin getUpdateInfo() { + public UpdateSite.Plugin getUpdateInfo() { UpdateCenter uc = Hudson.getInstance().getUpdateCenter(); - UpdateCenter.Plugin p = uc.getPlugin(getShortName()); + UpdateSite.Plugin p = uc.getPlugin(getShortName()); if(p!=null && p.isNewerThan(getVersion())) return p; return null; } /** - * returns the {@link UpdateCenter.Plugin} object, or null. + * returns the {@link UpdateSite.Plugin} object, or null. */ - public UpdateCenter.Plugin getInfo() { + public UpdateSite.Plugin getInfo() { UpdateCenter uc = Hudson.getInstance().getUpdateCenter(); return uc.getPlugin(getShortName()); } diff --git a/core/src/main/java/hudson/XmlFile.java b/core/src/main/java/hudson/XmlFile.java index 6ed07f8083d163d79ac797a74bc026c77941e50e..7318c16ed0a1e02469e7e39708e16b07388f1f98 100644 --- a/core/src/main/java/hudson/XmlFile.java +++ b/core/src/main/java/hudson/XmlFile.java @@ -140,7 +140,7 @@ public final class XmlFile { * * @return * The unmarshalled object. Usually the same as o, but would be different - * if the XML representation if completely new. + * if the XML representation is completely new. */ public Object unmarshal( Object o ) throws IOException { Reader r = new BufferedReader(new InputStreamReader(new FileInputStream(file),"UTF-8")); diff --git a/core/src/main/java/hudson/model/DownloadService.java b/core/src/main/java/hudson/model/DownloadService.java index f5b0beaa61825f4a2358fe5d953e9d5c5ec4fd9e..edaf8614b010a3e40188092e607f1f9692c3e272 100644 --- a/core/src/main/java/hudson/model/DownloadService.java +++ b/core/src/main/java/hudson/model/DownloadService.java @@ -152,7 +152,7 @@ public class DownloadService extends PageDecorator { * URL to download. */ public String getUrl() { - return Hudson.getInstance().getUpdateCenter().getUrl()+"updates/"+url; + return Hudson.getInstance().getUpdateCenter().getDefaultBaseUrl()+"updates/"+url; } /** diff --git a/core/src/main/java/hudson/model/Hudson.java b/core/src/main/java/hudson/model/Hudson.java index 5dd6dd42ba204060cfe807f5757a54d74f86ab33..60d6ecfa42b5db295228fb6fb22871dd4f82685f 100644 --- a/core/src/main/java/hudson/model/Hudson.java +++ b/core/src/main/java/hudson/model/Hudson.java @@ -490,7 +490,7 @@ public final class Hudson extends Node implements ItemGroup, Stapl */ private transient final String secretKey; - private transient final UpdateCenter updateCenter = new UpdateCenter(this); + private transient final UpdateCenter updateCenter = new UpdateCenter(); /** * True if the user opted out from the statistics tracking. We'll never send anything if this is true. @@ -522,7 +522,7 @@ public final class Hudson extends Node implements ItemGroup, Stapl Trigger.timer = new Timer("Hudson cron thread"); queue = new Queue(CONSISTENT_HASH?LoadBalancer.CONSISTENT_HASH:LoadBalancer.DEFAULT); - + try { dependencyGraph = DependencyGraph.EMPTY; } catch (InternalError e) { @@ -549,7 +549,7 @@ public final class Hudson extends Node implements ItemGroup, Stapl } catch (IOException e) { LOGGER.log(SEVERE, "Failed to load proxy configuration", e); } - + // load plugins. pluginManager = new PluginManager(context); pluginManager.initialize(); @@ -620,6 +620,8 @@ public final class Hudson extends Node implements ItemGroup, Stapl FileUtils.writeStringToFile(new File(userContentDir,"readme.txt"),Messages.Hudson_USER_CONTENT_README()); } + updateCenter.load(); // this has to wait until after all plugins load, to let custom UpdateCenterConfiguration take effect first. + Trigger.init(); // pending SEZPOZ-8 // // invoke post initialization methods diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java index 722b868cf321ce80c4e660a20ddf399202ac5e11..11962f43c8c180d5e4cf6ef5cb49976fbcd11a0e 100644 --- a/core/src/main/java/hudson/model/UpdateCenter.java +++ b/core/src/main/java/hudson/model/UpdateCenter.java @@ -23,69 +23,51 @@ */ package hudson.model; +import hudson.BulkChange; +import hudson.Extension; import hudson.ExtensionPoint; import hudson.Functions; import hudson.PluginManager; import hudson.PluginWrapper; -import hudson.Util; import hudson.ProxyConfiguration; -import hudson.Extension; +import hudson.Util; +import hudson.XmlFile; import hudson.lifecycle.Lifecycle; +import hudson.model.UpdateSite.Data; +import hudson.model.UpdateSite.Plugin; import hudson.util.DaemonThreadFactory; -import hudson.util.TextFile; -import hudson.util.VersionNumber; import hudson.util.IOException2; -import static hudson.util.TimeUnit2.DAYS; -import net.sf.json.JSONObject; +import hudson.util.PersistedList; +import hudson.util.XStream2; import org.acegisecurity.Authentication; -import org.apache.commons.io.input.CountingInputStream; import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.CountingInputStream; import org.apache.commons.io.output.NullOutputStream; -import org.apache.commons.io.output.TeeOutputStream; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; -import org.jvnet.hudson.crypto.SignatureOutputStream; -import org.jvnet.hudson.crypto.CertificateUtil; -import javax.servlet.ServletException; -import javax.servlet.ServletContext; 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.io.InputStream; -import java.io.ByteArrayInputStream; -import java.io.OutputStreamWriter; +import java.io.OutputStream; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; -import java.net.MalformedURLException; import java.util.ArrayList; -import java.util.Collections; +import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; -import java.util.TreeMap; import java.util.Vector; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; -import java.security.MessageDigest; -import java.security.DigestOutputStream; -import java.security.GeneralSecurityException; -import java.security.Signature; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.security.cert.TrustAnchor; -import java.security.cert.Certificate; - -import com.trilead.ssh2.crypto.Base64; /** * Controls update center capability. @@ -102,22 +84,7 @@ import com.trilead.ssh2.crypto.Base64; * @author Kohsuke Kawaguchi * @since 1.220 */ -public class UpdateCenter extends AbstractModelObject { - /** - * What's the time stamp of data file? - */ - private long dataTimestamp = -1; - - /** - * When was the last time we asked a browser to check the data for us? - * - *

- * There's normally some delay between when we send HTML that includes the check code, - * until we get the data back, so this variable is used to avoid asking too many browseres - * all at once. - */ - private volatile long lastAttempt = -1; - +public class UpdateCenter extends AbstractModelObject implements Saveable { /** * {@link ExecutorService} that performs installation. */ @@ -136,22 +103,30 @@ public class UpdateCenter extends AbstractModelObject { private final Vector jobs = new Vector(); /** - * Update center configuration data + * {@link UpdateSite}s from which we've already installed a plugin at least once. + * This is used to skip network tests. */ - private UpdateCenterConfiguration config; - + private final Set sourcesUsed = new HashSet(); + /** - * Create update center to get plugins/updates from hudson.dev.java.net + * List of {@link UpdateSite}s to be used. */ - public UpdateCenter(Hudson parent) { + private final PersistedList sites = new PersistedList(this); + + /** + * Update center configuration data + */ + private UpdateCenterConfiguration config; + + public UpdateCenter() { configure(new UpdateCenterConfiguration()); } - + /** * 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 */ @@ -160,19 +135,6 @@ public class UpdateCenter extends AbstractModelObject { this.config = config; } } - - /** - * Returns true if it's time for us to check for new version. - */ - public boolean isDue() { - if(neverUpdate) return false; - if(dataTimestamp==-1) - dataTimestamp = getDataFile().file.lastModified(); - long now = System.currentTimeMillis(); - boolean due = now - dataTimestamp > DAY && now - lastAttempt > 15000; - if(due) lastAttempt = now; - return due; - } /** * Returns the list of {@link UpdateCenterJob} representing scheduled installation attempts. @@ -187,90 +149,77 @@ public class UpdateCenter extends AbstractModelObject { } /** - * Gets the string representing how long ago the data was obtained. + * Returns the list of {@link UpdateSite}s to be used. + * This is a live list, whose change will be persisted automatically. + * + * @return + * can be empty but never null. */ - public String getLastUpdatedString() { - if(dataTimestamp<0) return "N/A"; - return Util.getPastTimeString(System.currentTimeMillis()-dataTimestamp); + public PersistedList getSites() { + return sites; } /** - * This is the endpoint that receives the update center data file from the browser. + * Gets the string representing how long ago the data was obtained. + * Will be the newest of all {@link UpdateSite}s. */ - public void doPostBack(StaplerRequest req) throws IOException, GeneralSecurityException { - dataTimestamp = System.currentTimeMillis(); - String p = req.getParameter("json"); - JSONObject o = JSONObject.fromObject(p); - - int v = o.getInt("updateCenterVersion"); - if(v !=1) { - LOGGER.warning("Unrecognized update center version: "+v); - return; + public String getLastUpdatedString() { + long newestTs = -1; + for (UpdateSite s : sites) { + if (s.getDataTimestamp()>newestTs) { + newestTs = s.getDataTimestamp(); + } } - - if (signatureCheck) - verifySignature(o); - - LOGGER.info("Obtained the latest update center data file"); - getDataFile().write(p); + if(newestTs<0) return "N/A"; + return Util.getPastTimeString(System.currentTimeMillis()-newestTs); } /** - * Verifies the signature in the update center data file. + * Gets {@link UpdateSite} by its ID. + * Used to bind them to URL. */ - private boolean verifySignature(JSONObject o) throws GeneralSecurityException, IOException { - JSONObject signature = o.getJSONObject("signature"); - if (signature.isNullObject()) { - LOGGER.severe("No signature block found"); - return false; - } - o.remove("signature"); - - List certs = new ArrayList(); - {// load and verify certificates - CertificateFactory cf = CertificateFactory.getInstance("X509"); - for (Object cert : o.getJSONArray("certificates")) { - X509Certificate c = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(Base64.decode(cert.toString().toCharArray()))); - c.checkValidity(); - certs.add(c); - } - - // all default root CAs in JVM are trusted, plus certs bundled in Hudson - Set anchors = CertificateUtil.getDefaultRootCAs(); - ServletContext context = Hudson.getInstance().servletContext; - for (String cert : (Set) context.getResourcePaths("/WEB-INF/update-center-rootCAs")) { - if (cert.endsWith(".txt")) continue; // skip text files that are meant to be documentation - anchors.add(new TrustAnchor((X509Certificate)cf.generateCertificate(context.getResourceAsStream(cert)),null)); + public UpdateSite getById(String id) { + for (UpdateSite s : sites) { + if (s.getId().equals(id)) { + return s; } - CertificateUtil.validatePath(certs); } + return null; + } - // this is for computing a digest to check sanity - MessageDigest sha1 = MessageDigest.getInstance("SHA1"); - DigestOutputStream dos = new DigestOutputStream(new NullOutputStream(),sha1); - - // this is for computing a signature - Signature sig = Signature.getInstance("SHA1withRSA"); - sig.initVerify(certs.get(0)); - SignatureOutputStream sos = new SignatureOutputStream(sig); - - o.writeCanonical(new OutputStreamWriter(new TeeOutputStream(dos,sos),"UTF-8")); + /** + * Gets the {@link UpdateSite} from which we receive updates for hudson.war. + * + * @return + * null if no such update center is provided. + */ + public UpdateSite getCoreSource() { + for (UpdateSite s : sites) + if (s.getData().core!=null) + return s; + return null; + } - // did the digest match? this is not a part of the signature validation, but if we have a bug in the c14n - // (which is more likely than someone tampering with update center), we can tell - String computedDigest = new String(Base64.encode(sha1.digest())); - String providedDigest = signature.getString("digest"); - if (!computedDigest.equalsIgnoreCase(providedDigest)) { - LOGGER.severe("Digest mismatch: "+computedDigest+" vs "+providedDigest); - return false; - } + /** + * Gets the default base URL. + * + * @deprecated + * TODO: revisit tool update mechanism, as that should be de-centralized, too. In the mean time, + * please try not to use this method, and instead ping us to get this part completed. + */ + public String getDefaultBaseUrl() { + return config.getUpdateCenterUrl(); + } - if (!sig.verify(Base64.decode(signature.getString("signature").toCharArray()))) { - LOGGER.severe("Signature in the update center doesn't match with the certificate"); - return false; + /** + * Gets the plugin with the given name from the first {@link UpdateSite} to contain it. + */ + public Plugin getPlugin(String artifactId) { + for (UpdateSite s : sites) { + Plugin p = s.getPlugin(artifactId); + if (p!=null) return p; } - - return true; + return null; } /** @@ -279,7 +228,7 @@ public class UpdateCenter extends AbstractModelObject { public void doUpgrade(StaplerResponse rsp) throws IOException, ServletException { requirePOST(); Hudson.getInstance().checkPermission(Hudson.ADMINISTER); - HudsonUpgradeJob job = new HudsonUpgradeJob(Hudson.getAuthentication()); + HudsonUpgradeJob job = new HudsonUpgradeJob(getCoreSource(), Hudson.getAuthentication()); if(!Lifecycle.get().canRewriteHudsonWar()) { sendError("Hudson upgrade not supported in this running mode"); return; @@ -290,120 +239,78 @@ public class UpdateCenter extends AbstractModelObject { rsp.sendRedirect2("."); } - private Future addJob(UpdateCenterJob job) { + /*package*/ synchronized Future addJob(UpdateCenterJob job) { // the first job is always the connectivity check - if(jobs.size()==0) - new ConnectionCheckJob().submit(); + if (sourcesUsed.add(job.site)) + new ConnectionCheckJob(job.site).submit(); return job.submit(); } - /** - * Loads the update center data, if any. - * - * @return null if no data is available. - */ - public Data getData() { - TextFile df = getDataFile(); - if(df.exists()) { - try { - return new Data(JSONObject.fromObject(df.read())); - } catch (IOException e) { - LOGGER.log(Level.SEVERE,"Failed to parse "+df,e); - df.delete(); // if we keep this file, it will cause repeated failures - return null; - } - } else { - return null; - } + public String getDisplayName() { + return "Update center"; + } + + public String getSearchUrl() { + return "updateCenter"; } /** - * Returns a list of plugins that should be shown in the "available" tab. - * These are "all plugins - installed plugins". + * Saves the configuration info to the disk. */ - public List getAvailables() { - List r = new ArrayList(); - Data data = getData(); - if(data ==null) return Collections.emptyList(); - for (Plugin p : data.plugins.values()) { - if(p.getInstalled()==null) - r.add(p); + public synchronized void save() { + if(BulkChange.contains(this)) return; + try { + getConfigFile().write(sites); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to save "+getConfigFile(),e); } - return r; } /** - * Gets the information about a specific plugin. - * - * @param artifactId - * The short name of the plugin. Corresponds to {@link PluginWrapper#getShortName()}. - * - * @return - * null if no such information is found. + * Loads the data from the disk into this object. */ - public Plugin getPlugin(String artifactId) { - Data dt = getData(); - if(dt==null) return null; - return dt.plugins.get(artifactId); + public synchronized void load() throws IOException { + XmlFile file = getConfigFile(); + if(file.exists()) { + try { + sites.replaceBy(((PersistedList)file.unmarshal(sites)).toList()); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to load "+file, e); + } + } else { + // If there aren't already any UpdateSources, add the default one. + if (sites.isEmpty()) { + // to maintain compatibility with existing UpdateCenterConfiguration, create the default one as specified by UpdateCenterConfiguration + sites.add(new UpdateSite("default",config.getUpdateCenterUrl()+"update-center.json")); + } + } } - /** - * This is where we store the update center data. - */ - private TextFile getDataFile() { - return new TextFile(new File(Hudson.getInstance().root,"update-center.json")); + private XmlFile getConfigFile() { + return new XmlFile(XSTREAM,new File(Hudson.getInstance().root, + UpdateCenter.class.getName()+".xml")); } - /** - * Returns the list of plugins that are updates to currently installed ones. - * - * @return - * can be empty but never null. - */ - public List getUpdates() { - Data data = getData(); - if(data==null) return Collections.emptyList(); // fail to determine + public List getAvailables() { + List plugins = new ArrayList(); - List r = new ArrayList(); - for (PluginWrapper pw : Hudson.getInstance().getPluginManager().getPlugins()) { - Plugin p = pw.getUpdateInfo(); - if(p!=null) r.add(p); + for (UpdateSite s : sites) { + plugins.addAll(s.getAvailables()); } - return r; + return plugins; } - /** - * Does any of the plugin has updates? - */ - public boolean hasUpdates() { - Data data = getData(); - if(data==null) return false; - - for (PluginWrapper pw : Hudson.getInstance().getPluginManager().getPlugins()) { - if(!pw.isBundled() && pw.getUpdateInfo()!=null) - // do not advertize updates to bundled plugins, since we generally want users to get them - // as a part of hudson.war updates. This also avoids unnecessary pinning of plugins. - return true; - } - return false; - } + public List getUpdates() { + List plugins = new ArrayList(); - public String getDisplayName() { - return "Update center"; - } + for (UpdateSite s : sites) { + plugins.addAll(s.getUpdates()); + } - public String getSearchUrl() { - return "updateCenter"; + return plugins; } - /** - * Exposed to get rid of hardcoding of the URL that serves up update-center.json - * in Javascript. - */ - public String getUrl() { - return config.getUpdateCenterUrl(); - } /** * {@link AdministrativeMonitor} that checks if there's Hudson update. @@ -416,213 +323,39 @@ public class UpdateCenter extends AbstractModelObject { } public Data getData() { - return Hudson.getInstance().getUpdateCenter().getData(); + UpdateSite cs = Hudson.getInstance().getUpdateCenter().getCoreSource(); + if (cs!=null) return cs.getData(); + return null; } } + /** - * In-memory representation of the update center data. + * Strategy object for controlling the update center's behaviors. + * + *

+ * Until 1.MULTIUPDATE, this extension point used to control the configuration of + * where to get updates (hence the name of this class), but with the introduction + * of multiple update center sites capability, that functionality is achieved by + * simply installing another {@link UpdateSite}. + * + *

+ * See {@link UpdateSite} for how to manipulate them programmatically. + * + * @since 1.266 */ - public final class Data { - /** - * The latest hudson.war. - */ - public final Entry core; - /** - * Plugins in the official repository, keyed by their artifact IDs. - */ - public final Map plugins = new TreeMap(String.CASE_INSENSITIVE_ORDER); - - Data(JSONObject o) { - core = new Entry(o.getJSONObject("core")); - for(Map.Entry e : (Set>)o.getJSONObject("plugins").entrySet()) { - plugins.put(e.getKey(),new Plugin(e.getValue())); - } - } - - /** - * Is there a new version of the core? - */ - public boolean hasCoreUpdates() { - return core.isNewerThan(Hudson.VERSION); - } - - /** - * Do we support upgrade? - */ - public boolean canUpgrade() { - return Lifecycle.get().canRewriteHudsonWar(); - } - } - - public static class Entry { - /** - * Artifact ID. - */ - public final String name; - /** - * The version. - */ - public final String version; - /** - * Download URL. - */ - public final String url; - - public Entry(JSONObject o) { - this.name = o.getString("name"); - this.version = o.getString("version"); - this.url = o.getString("url"); - } - - /** - * Checks if the specified "current version" is older than the version of this entry. - * - * @param currentVersion - * The string that represents the version number to be compared. - * @return - * true if the version listed in this entry is newer. - * false otherwise, including the situation where the strings couldn't be parsed as version numbers. - */ - public boolean isNewerThan(String currentVersion) { - return isNewerThan(currentVersion, version); - } - - /** - * Compares two versions - returns true if the first version is newer than the second. - * - * @param firstVersion - * The first version to test against. - * @param secondVersion - * The second version to test against. - * @return - * True if the first version is newer than the second version. False in all other cases. - */ - public boolean isNewerThan(String firstVersion, String secondVersion) { - try { - return new VersionNumber(firstVersion).compareTo(new VersionNumber(secondVersion)) < 0; - } catch (IllegalArgumentException e) { - // couldn't parse as the version number. - return false; - } - } - } - - public final class Plugin extends Entry { - /** - * Optional URL to the Wiki page that discusses this plugin. - */ - public final String wiki; - /** - * Human readable title of the plugin, taken from Wiki page. - * Can be null. - * - *

- * beware of XSS vulnerability since this data comes from Wiki - */ - public final String title; - /** - * Optional excerpt string. - */ - public final String excerpt; - /** - * Optional version # from which this plugin release is configuration-compatible. - */ - public final String compatibleSinceVersion; - - @DataBoundConstructor - public Plugin(JSONObject o) { - super(o); - this.wiki = get(o,"wiki"); - this.title = get(o,"title"); - this.excerpt = get(o,"excerpt"); - this.compatibleSinceVersion = get(o,"compatibleSinceVersion"); - } - - private String get(JSONObject o, String prop) { - if(o.has(prop)) - return o.getString(prop); - else - return null; - } - - public String getDisplayName() { - if(title!=null) return title; - return name; - } - - /** - * If some version of this plugin is currently installed, return {@link PluginWrapper}. - * Otherwise null. - */ - public PluginWrapper getInstalled() { - PluginManager pm = Hudson.getInstance().getPluginManager(); - return pm.getPlugin(name); - } - - /** - * If the plugin is already installed, and the new version of the plugin has a "compatibleSinceVersion" - * value (i.e., it's only directly compatible with that version or later), this will check to - * see if the installed version is older than the compatible-since version. If it is older, it'll return false. - * If it's not older, or it's not installed, or it's installed but there's no compatibleSinceVersion - * specified, it'll return true. - */ - public boolean isCompatibleWithInstalledVersion() { - PluginWrapper installedVersion = getInstalled(); - if (installedVersion != null) { - if (compatibleSinceVersion != null) { - if (new VersionNumber(installedVersion.getVersion()) - .isOlderThan(new VersionNumber(compatibleSinceVersion))) { - return false; - } - } - } - return true; - } - - /** - * @deprecated as of 1.326 - * Use {@link #deploy()}. - */ - public void install() { - deploy(); - } - - /** - * Schedules the installation of this plugin. - * - *

- * This is mainly intended to be called from the UI. The actual installation work happens - * asynchronously in another thread. - */ - public Future deploy() { - Hudson.getInstance().checkPermission(Hudson.ADMINISTER); - return addJob(new InstallationJob(this, Hudson.getAuthentication())); - } - + @SuppressWarnings({"UnusedDeclaration"}) + public static class UpdateCenterConfiguration implements ExtensionPoint { /** - * Making the installation web bound. + * Creates default update center configuration - uses settings for global update center. */ - public void doInstall(StaplerResponse rsp) throws IOException { - install(); - rsp.sendRedirect2("../.."); + public UpdateCenterConfiguration() { } - } - /** - * Configuration data for controlling the update center's behaviors. The update - * center's defaults will check internet connectivity by trying to connect - * to www.google.com; will download plugins, the plugin catalog and updates - * from hudson-ci.org; and will install plugins with file system - * operations. - * - * @since 1.266 - */ - public static class UpdateCenterConfiguration implements ExtensionPoint { /** * Check network connectivity by trying to establish a connection to * the host in connectionCheckUrl. - * + * * @param job The connection checker that is invoking this strategy. * @param connectionCheckUrl A string containing the URL of a domain * that is assumed to be always available. @@ -631,10 +364,10 @@ public class UpdateCenter extends AbstractModelObject { public void checkConnection(ConnectionCheckJob job, String connectionCheckUrl) throws IOException { testConnection(new URL(connectionCheckUrl)); } - + /** * Check connection to update center server. - * + * * @param job The connection checker that is invoking this strategy. * @param updateCenterUrl A sting containing the URL of the update center host. * @throws IOException if a connection to the update center server can't be established. @@ -642,29 +375,22 @@ public class UpdateCenter extends AbstractModelObject { public void checkUpdateCenter(ConnectionCheckJob job, String updateCenterUrl) throws IOException { testConnection(new URL(updateCenterUrl + "?uctest")); } - + /** - * Validate the URL of the resource before downloading it. The default - * implementation enforces that the base of the resource URL starts - * with the string returned by {@link #getPluginRepositoryBaseUrl()}. - * + * Validate the URL of the resource before downloading it. + * * @param job The download job that is invoking this strategy. This job is * responsible for managing the status of the download and installation. * @param src The location of the resource on the network * @throws IOException if the validation fails */ public void preValidate(DownloadJob job, URL src) throws IOException { - // In the future if we are to open up update center to 3rd party, we need more elaborate scheme - // like signing to ensure the safety of the bits. - if(!src.toExternalForm().startsWith(getPluginRepositoryBaseUrl())) { - throw new IOException("Installation of plugin from "+src+" is not allowed"); - } } - + /** * Validate the resource after it has been downloaded, before it is * installed. The default implementation does nothing. - * + * * @param job The download job that is invoking this strategy. This job is * responsible for managing the status of the download and installation. * @param src The location of the downloaded resource. @@ -672,13 +398,13 @@ public class UpdateCenter extends AbstractModelObject { */ public void postValidate(DownloadJob job, File src) throws IOException { } - + /** * Download a plugin or core upgrade in preparation for installing it * into its final location. Implementations will normally download the * resource into a temporary location and hand off a reference to this * location to the install or upgrade strategy to move into the final location. - * + * * @param job The download job that is invoking this strategy. This job is * responsible for managing the status of the download and installation. * @param src The URL to the resource to be downloaded. @@ -716,14 +442,14 @@ public class UpdateCenter extends AbstractModelObject { // indicates that this kind of inconsistency can happen. So let's be defensive throw new IOException("Inconsistent file length: expected "+total+" but only got "+tmp.length()); } - + return tmp; } - + /** * Called after a plugin has been downloaded to move it into its final * location. The default implementation is a file rename. - * + * * @param job The install job that is invoking this strategy. * @param src The temporary location of the plugin. * @param dst The final destination to install the plugin to. @@ -732,11 +458,11 @@ public class UpdateCenter extends AbstractModelObject { public void install(DownloadJob job, File src, File dst) throws IOException { job.replace(dst, src); } - + /** * Called after an upgrade has been downloaded to move it into its final * location. The default implementation is a file rename. - * + * * @param job The upgrade job that is invoking this strategy. * @param src The temporary location of the upgrade. * @param dst The final destination to install the upgrade to. @@ -744,34 +470,46 @@ public class UpdateCenter extends AbstractModelObject { */ public void upgrade(DownloadJob job, File src, File dst) throws IOException { job.replace(dst, src); - } + } /** - * Returns an "always up" server for Internet connectivity testing + * Returns an "always up" server for Internet connectivity testing. + * + * @deprecated as of 1.MULTIUPDATE + * With the introduction of multiple update center capability, this information + * is now a part of the update-center.json file. See + * http://hudson-ci.org/update-center.json as an example. */ public String getConnectionCheckUrl() { return "http://www.google.com"; } - + /** * Returns the URL of the server that hosts the update-center.json * file. * + * @deprecated as of 1.MULTIUPDATE + * With the introduction of multiple update center capability, this information + * is now moved to {@link UpdateSite}. * @return * Absolute URL that ends with '/'. */ public String getUpdateCenterUrl() { return "http://hudson-ci.org/"; } - + /** * Returns the URL of the server that hosts plugins and core updates. + * + * @deprecated as of 1.MULTIUPDATE + * update-center.json is now signed, so we don't have to further make sure that + * we aren't downloading from anywhere unsecure. */ public String getPluginRepositoryBaseUrl() { return "http://hudson-ci.org/"; } - + private void testConnection(URL url) throws IOException { try { InputStream in = ProxyConfiguration.open(url).getInputStream(); @@ -782,15 +520,24 @@ public class UpdateCenter extends AbstractModelObject { // fix up this crappy error message from JDK throw new IOException2("Failed to validate the SSL certificate of "+url,e); } - } + } } - + /** * Things that {@link UpdateCenter#installerService} executes. * * This object will have the row.jelly which renders the job on UI. */ public abstract class UpdateCenterJob implements Runnable { + /** + * Which {@link UpdateSite} does this belong to? + */ + public final UpdateSite site; + + protected UpdateCenterJob(UpdateSite site) { + this.site = site; + } + /** * @deprecated as of 1.326 * Use {@link #submit()} instead. @@ -817,24 +564,29 @@ public class UpdateCenter extends AbstractModelObject { public final class ConnectionCheckJob extends UpdateCenterJob { private final Vector statuses= new Vector(); + public ConnectionCheckJob(UpdateSite site) { + super(site); + } + public void run() { LOGGER.fine("Doing a connectivity check"); try { - String connectionCheckUrl = config.getConnectionCheckUrl(); - - statuses.add(Messages.UpdateCenter_Status_CheckingInternet()); - try { - config.checkConnection(this, connectionCheckUrl); - } catch (IOException e) { - if(e.getMessage().contains("Connection timed out")) { - // Google can't be down, so this is probably a proxy issue - statuses.add(Messages.UpdateCenter_Status_ConnectionFailed(connectionCheckUrl)); - return; + String connectionCheckUrl = site.getConnectionCheckUrl(); + if (connectionCheckUrl!=null) { + statuses.add(Messages.UpdateCenter_Status_CheckingInternet()); + try { + config.checkConnection(this, connectionCheckUrl); + } catch (IOException e) { + if(e.getMessage().contains("Connection timed out")) { + // Google can't be down, so this is probably a proxy issue + statuses.add(Messages.UpdateCenter_Status_ConnectionFailed(connectionCheckUrl)); + return; + } } } statuses.add(Messages.UpdateCenter_Status_CheckingJavaNet()); - config.checkUpdateCenter(this, config.getUpdateCenterUrl()); + config.checkUpdateCenter(this, site.getUrl()); statuses.add(Messages.UpdateCenter_Status_Success()); } catch (UnknownHostException e) { @@ -886,9 +638,9 @@ public class UpdateCenter extends AbstractModelObject { */ protected abstract void onSuccess(); - + private Authentication authentication; - + /** * Get the user that initiated this job */ @@ -896,18 +648,18 @@ public class UpdateCenter extends AbstractModelObject { { return this.authentication; } - - protected DownloadJob(Authentication authentication) - { + + protected DownloadJob(UpdateSite site, Authentication authentication) { + super(site); this.authentication = authentication; } - + public void run() { try { LOGGER.info("Starting the installation of "+getName()+" on behalf of "+getUser().getName()); _run(); - + LOGGER.info("Installation successful: "+getName()); status = new Success(); onSuccess(); @@ -1005,8 +757,8 @@ public class UpdateCenter extends AbstractModelObject { private final PluginManager pm = Hudson.getInstance().getPluginManager(); - public InstallationJob(Plugin plugin, Authentication auth) { - super(auth); + public InstallationJob(Plugin plugin, UpdateSite site, Authentication auth) { + super(site, auth); this.plugin = plugin; } @@ -1047,12 +799,12 @@ public class UpdateCenter extends AbstractModelObject { * Represents the state of the upgrade activity of Hudson core. */ public final class HudsonUpgradeJob extends DownloadJob { - public HudsonUpgradeJob(Authentication auth) { - super(auth); + public HudsonUpgradeJob(UpdateSite site, Authentication auth) { + super(site, auth); } protected URL getURL() throws MalformedURLException { - return new URL(getData().core.url); + return new URL(site.getData().core.url); } protected File getDestination() { @@ -1088,14 +840,13 @@ public class UpdateCenter extends AbstractModelObject { */ private static final AtomicInteger iota = new AtomicInteger(); - private static final long DAY = DAYS.toMillis(1); - private static final Logger LOGGER = Logger.getLogger(UpdateCenter.class.getName()); public static boolean neverUpdate = Boolean.getBoolean(UpdateCenter.class.getName()+".never"); - /** - * Off by default until we know this is reasonably working. - */ - public static boolean signatureCheck = Boolean.getBoolean(UpdateCenter.class.getName()+".signatureCheck"); + public static final XStream2 XSTREAM = new XStream2(); + static { + XSTREAM.alias("site",UpdateSite.class); + XSTREAM.alias("sites",PersistedList.class); + } } diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java new file mode 100644 index 0000000000000000000000000000000000000000..e6d31442d670cca58f6d63e2cf839994a7752338 --- /dev/null +++ b/core/src/main/java/hudson/model/UpdateSite.java @@ -0,0 +1,558 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! Inc., Seiji Sogabe, + * Andrew Bayer + * + * 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.PluginWrapper; +import hudson.PluginManager; +import hudson.model.UpdateCenter.UpdateCenterJob; +import hudson.lifecycle.Lifecycle; +import hudson.util.TextFile; +import hudson.util.VersionNumber; +import static hudson.util.TimeUnit2.DAYS; +import net.sf.json.JSONObject; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.StaplerResponse; +import org.jvnet.hudson.crypto.CertificateUtil; +import org.jvnet.hudson.crypto.SignatureOutputStream; +import org.apache.commons.io.output.NullOutputStream; +import org.apache.commons.io.output.TeeOutputStream; + +import java.io.File; +import java.io.IOException; +import java.io.ByteArrayInputStream; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.Set; +import java.util.concurrent.Future; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.DigestOutputStream; +import java.security.Signature; +import java.security.cert.X509Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.TrustAnchor; + +import com.trilead.ssh2.crypto.Base64; + +import javax.servlet.ServletContext; + + +/** + * Source of the update center information, like "http://hudson-ci.org/update-center.json" + * + *

+ * Hudson can have multiple {@link UpdateSite}s registered in the system, so that it can pick up plugins + * from different locations. + * + * @author Andrew Bayer + * @author Kohsuke Kawaguchi + * @since 1.MULTIUPDATE + */ +public class UpdateSite { + /** + * What's the time stamp of data file? + */ + private transient long dataTimestamp = -1; + + /** + * When was the last time we asked a browser to check the data for us? + * + *

+ * There's normally some delay between when we send HTML that includes the check code, + * until we get the data back, so this variable is used to avoid asking too many browseres + * all at once. + */ + private transient volatile long lastAttempt = -1; + + /** + * ID string for this update source. + */ + private final String id; + + /** + * Path to update-center.json, like http://hudson-ci.org/update-center.json. + */ + private final String url; + + public UpdateSite(String id, String url) { + this.id = id; + this.url = url; + } + + /** + * When read back from XML, initialize them back to -1. + */ + private Object readResolve() { + dataTimestamp = lastAttempt = -1; + return this; + } + + /** + * Get ID string. + */ + public String getId() { + return id; + } + + public long getDataTimestamp() { + return dataTimestamp; + } + + /** + * This is the endpoint that receives the update center data file from the browser. + */ + public void doPostBack(@QueryParameter String json) throws IOException, GeneralSecurityException { + dataTimestamp = System.currentTimeMillis(); + JSONObject o = JSONObject.fromObject(json); + + int v = o.getInt("updateCenterVersion"); + if(v !=1) { + LOGGER.warning("Unrecognized update center version: "+v); + return; + } + + if (signatureCheck) + verifySignature(o); + + LOGGER.info("Obtained the latest update center data file for UpdateSource "+ id); + getDataFile().write(json); + } + + /** + * Verifies the signature in the update center data file. + */ + private boolean verifySignature(JSONObject o) throws GeneralSecurityException, IOException { + JSONObject signature = o.getJSONObject("signature"); + if (signature.isNullObject()) { + LOGGER.severe("No signature block found"); + return false; + } + o.remove("signature"); + + List certs = new ArrayList(); + {// load and verify certificates + CertificateFactory cf = CertificateFactory.getInstance("X509"); + for (Object cert : o.getJSONArray("certificates")) { + X509Certificate c = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(Base64.decode(cert.toString().toCharArray()))); + c.checkValidity(); + certs.add(c); + } + + // all default root CAs in JVM are trusted, plus certs bundled in Hudson + Set anchors = CertificateUtil.getDefaultRootCAs(); + ServletContext context = Hudson.getInstance().servletContext; + for (String cert : (Set) context.getResourcePaths("/WEB-INF/update-center-rootCAs")) { + if (cert.endsWith(".txt")) continue; // skip text files that are meant to be documentation + anchors.add(new TrustAnchor((X509Certificate)cf.generateCertificate(context.getResourceAsStream(cert)),null)); + } + CertificateUtil.validatePath(certs); + } + + // this is for computing a digest to check sanity + MessageDigest sha1 = MessageDigest.getInstance("SHA1"); + DigestOutputStream dos = new DigestOutputStream(new NullOutputStream(),sha1); + + // this is for computing a signature + Signature sig = Signature.getInstance("SHA1withRSA"); + sig.initVerify(certs.get(0)); + SignatureOutputStream sos = new SignatureOutputStream(sig); + + o.writeCanonical(new OutputStreamWriter(new TeeOutputStream(dos,sos),"UTF-8")); + + // did the digest match? this is not a part of the signature validation, but if we have a bug in the c14n + // (which is more likely than someone tampering with update center), we can tell + String computedDigest = new String(Base64.encode(sha1.digest())); + String providedDigest = signature.getString("digest"); + if (!computedDigest.equalsIgnoreCase(providedDigest)) { + LOGGER.severe("Digest mismatch: "+computedDigest+" vs "+providedDigest); + return false; + } + + if (!sig.verify(Base64.decode(signature.getString("signature").toCharArray()))) { + LOGGER.severe("Signature in the update center doesn't match with the certificate"); + return false; + } + + return true; + } + + /** + * Returns true if it's time for us to check for new version. + */ + public boolean isDue() { + if(neverUpdate) return false; + if(dataTimestamp==-1) + dataTimestamp = getDataFile().file.lastModified(); + long now = System.currentTimeMillis(); + boolean due = now - dataTimestamp > DAY && now - lastAttempt > 15000; + if(due) lastAttempt = now; + return due; + } + + /** + * Loads the update center data, if any. + * + * @return null if no data is available. + */ + public Data getData() { + TextFile df = getDataFile(); + if(df.exists()) { + try { + return new Data(JSONObject.fromObject(df.read())); + } catch (IOException e) { + LOGGER.log(Level.SEVERE,"Failed to parse "+df,e); + df.delete(); // if we keep this file, it will cause repeated failures + return null; + } + } else { + return null; + } + } + + /** + * Returns a list of plugins that should be shown in the "available" tab. + * These are "all plugins - installed plugins". + */ + public List getAvailables() { + List r = new ArrayList(); + Data data = getData(); + if(data ==null) return Collections.emptyList(); + for (Plugin p : data.plugins.values()) { + if(p.getInstalled()==null) + r.add(p); + } + return r; + } + + /** + * Gets the information about a specific plugin. + * + * @param artifactId + * The short name of the plugin. Corresponds to {@link PluginWrapper#getShortName()}. + * + * @return + * null if no such information is found. + */ + public Plugin getPlugin(String artifactId) { + Data dt = getData(); + if(dt==null) return null; + return dt.plugins.get(artifactId); + } + + /** + * Returns an "always up" server for Internet connectivity testing, or null if we are going to skip the test. + */ + public String getConnectionCheckUrl() { + Data dt = getData(); + if(dt==null) return "http://www.google.com/"; + return dt.connectionCheckUrl; + } + + /** + * This is where we store the update center data. + */ + private TextFile getDataFile() { + return new TextFile(new File(Hudson.getInstance().getRootDir(), + "updates/" + getId()+".json")); + } + + /** + * Returns the list of plugins that are updates to currently installed ones. + * + * @return + * can be empty but never null. + */ + public List getUpdates() { + Data data = getData(); + if(data==null) return Collections.emptyList(); // fail to determine + + List r = new ArrayList(); + for (PluginWrapper pw : Hudson.getInstance().getPluginManager().getPlugins()) { + Plugin p = pw.getUpdateInfo(); + if(p!=null) r.add(p); + } + + return r; + } + + /** + * Does any of the plugin has updates? + */ + public boolean hasUpdates() { + Data data = getData(); + if(data==null) return false; + + for (PluginWrapper pw : Hudson.getInstance().getPluginManager().getPlugins()) { + if(!pw.isBundled() && pw.getUpdateInfo()!=null) + // do not advertize updates to bundled plugins, since we generally want users to get them + // as a part of hudson.war updates. This also avoids unnecessary pinning of plugins. + return true; + } + return false; + } + + + /** + * Exposed to get rid of hardcoding of the URL that serves up update-center.json + * in Javascript. + */ + public String getUrl() { + return url; + } + + /** + * In-memory representation of the update center data. + */ + public final class Data { + /** + * The {@link UpdateSite} ID. + */ + public final String sourceId; + + /** + * The latest hudson.war. + */ + public final Entry core; + /** + * Plugins in the repository, keyed by their artifact IDs. + */ + public final Map plugins = new TreeMap(String.CASE_INSENSITIVE_ORDER); + + /** + * If this is non-null, Hudson is going to check the connectivity to this URL to make sure + * the network connection is up. Null to skip the check. + */ + public final String connectionCheckUrl; + + Data(JSONObject o) { + this.sourceId = (String)o.get("id"); + if (sourceId.equals("default")) { + core = new Entry(sourceId, o.getJSONObject("core")); + } + else { + core = null; + } + for(Map.Entry e : (Set>)o.getJSONObject("plugins").entrySet()) { + plugins.put(e.getKey(),new Plugin(sourceId, e.getValue())); + } + + connectionCheckUrl = (String)o.get("connectionCheckUrl"); + } + + /** + * Is there a new version of the core? + */ + public boolean hasCoreUpdates() { + return core != null && core.isNewerThan(Hudson.VERSION); + } + + /** + * Do we support upgrade? + */ + public boolean canUpgrade() { + return Lifecycle.get().canRewriteHudsonWar(); + } + } + + public static class Entry { + /** + * {@link UpdateSite} ID. + */ + public final String sourceId; + + /** + * Artifact ID. + */ + public final String name; + /** + * The version. + */ + public final String version; + /** + * Download URL. + */ + public final String url; + + public Entry(String sourceId, JSONObject o) { + this.sourceId = sourceId; + this.name = o.getString("name"); + this.version = o.getString("version"); + this.url = o.getString("url"); + } + + /** + * Checks if the specified "current version" is older than the version of this entry. + * + * @param currentVersion + * The string that represents the version number to be compared. + * @return + * true if the version listed in this entry is newer. + * false otherwise, including the situation where the strings couldn't be parsed as version numbers. + */ + public boolean isNewerThan(String currentVersion) { + return isNewerThan(currentVersion, version); + } + + /** + * Compares two versions - returns true if the first version is newer than the second. + * + * @param firstVersion + * The first version to test against. + * @param secondVersion + * The second version to test against. + * @return + * True if the first version is newer than the second version. False in all other cases. + */ + private static boolean isNewerThan(String firstVersion, String secondVersion) { + try { + return new VersionNumber(firstVersion).compareTo(new VersionNumber(secondVersion)) < 0; + } catch (IllegalArgumentException e) { + // couldn't parse as the version number. + return false; + } + } + } + + public final class Plugin extends Entry { + /** + * Optional URL to the Wiki page that discusses this plugin. + */ + public final String wiki; + /** + * Human readable title of the plugin, taken from Wiki page. + * Can be null. + * + *

+ * beware of XSS vulnerability since this data comes from Wiki + */ + public final String title; + /** + * Optional excerpt string. + */ + public final String excerpt; + /** + * Optional version # from which this plugin release is configuration-compatible. + */ + public final String compatibleSinceVersion; + + @DataBoundConstructor + public Plugin(String sourceId, JSONObject o) { + super(sourceId, o); + this.wiki = get(o,"wiki"); + this.title = get(o,"title"); + this.excerpt = get(o,"excerpt"); + this.compatibleSinceVersion = get(o,"compatibleSinceVersion"); + } + + private String get(JSONObject o, String prop) { + if(o.has(prop)) + return o.getString(prop); + else + return null; + } + + public String getDisplayName() { + if(title!=null) return title; + return name; + } + + /** + * If some version of this plugin is currently installed, return {@link PluginWrapper}. + * Otherwise null. + */ + public PluginWrapper getInstalled() { + PluginManager pm = Hudson.getInstance().getPluginManager(); + return pm.getPlugin(name); + } + + /** + * If the plugin is already installed, and the new version of the plugin has a "compatibleSinceVersion" + * value (i.e., it's only directly compatible with that version or later), this will check to + * see if the installed version is older than the compatible-since version. If it is older, it'll return false. + * If it's not older, or it's not installed, or it's installed but there's no compatibleSinceVersion + * specified, it'll return true. + */ + public boolean isCompatibleWithInstalledVersion() { + PluginWrapper installedVersion = getInstalled(); + if (installedVersion != null) { + if (compatibleSinceVersion != null) { + if (new VersionNumber(installedVersion.getVersion()) + .isOlderThan(new VersionNumber(compatibleSinceVersion))) { + return false; + } + } + } + return true; + } + + /** + * @deprecated as of 1.326 + * Use {@link #deploy()}. + */ + public void install() { + deploy(); + } + + /** + * Schedules the installation of this plugin. + * + *

+ * This is mainly intended to be called from the UI. The actual installation work happens + * asynchronously in another thread. + */ + public Future deploy() { + Hudson.getInstance().checkPermission(Hudson.ADMINISTER); + UpdateCenter uc = Hudson.getInstance().getUpdateCenter(); + return uc.addJob(uc.new InstallationJob(this, UpdateSite.this, Hudson.getAuthentication())); + } + + /** + * Making the installation web bound. + */ + public void doInstall(StaplerResponse rsp) throws IOException { + deploy(); + rsp.sendRedirect2("../.."); + } + } + + private static final long DAY = DAYS.toMillis(1); + + private static final Logger LOGGER = Logger.getLogger(UpdateSite.class.getName()); + + public static boolean neverUpdate = Boolean.getBoolean(UpdateCenter.class.getName()+".never"); + + /** + * Off by default until we know this is reasonably working. + */ + public static boolean signatureCheck = Boolean.getBoolean(UpdateCenter.class.getName()+".signatureCheck"); +} diff --git a/core/src/main/java/hudson/util/DescribableList.java b/core/src/main/java/hudson/util/DescribableList.java index df0988024fe680990617a4159b7003dc60f2a227..2597e0369ac3507be6caa2244078c96e84dc7d43 100644 --- a/core/src/main/java/hudson/util/DescribableList.java +++ b/core/src/main/java/hudson/util/DescribableList.java @@ -61,10 +61,7 @@ import java.util.Map; * * @author Kohsuke Kawaguchi */ -public class DescribableList, D extends Descriptor> implements Iterable { - private final CopyOnWriteList data = new CopyOnWriteList(); - private Saveable owner; - +public class DescribableList, D extends Descriptor> extends PersistedList { protected DescribableList() { } @@ -88,20 +85,6 @@ public class DescribableList, D extends Descriptor> this.owner = owner; } - public void setOwner(Saveable owner) { - this.owner = owner; - } - - public void add(T item) throws IOException { - data.add(item); - onModified(); - } - - public void addAll(Collection items) throws IOException { - data.addAll(items); - onModified(); - } - /** * Removes all instances of the same type, then add the new one. */ @@ -111,11 +94,6 @@ public class DescribableList, D extends Descriptor> onModified(); } - public void replaceBy(Collection col) throws IOException { - data.replaceBy(col); - onModified(); - } - public T get(D descriptor) { for (T t : data) if(t.getDescriptor()==descriptor) @@ -123,57 +101,10 @@ public class DescribableList, D extends Descriptor> return null; } - public U get(Class type) { - for (T t : data) - if(type.isInstance(t)) - return type.cast(t); - return null; - } - - /** - * Gets all instances that matches the given type. - */ - public List getAll(Class type) { - List r = new ArrayList(); - for (T t : data) - if(type.isInstance(t)) - r.add(type.cast(t)); - return r; - } - public boolean contains(D d) { return get(d)!=null; } - public int size() { - return data.size(); - } - - /** - * Removes an instance by its type. - */ - public void remove(Class type) throws IOException { - for (T t : data) { - if(t.getClass()==type) { - data.remove(t); - onModified(); - return; - } - } - } - - public void removeAll(Class type) throws IOException { - boolean modified=false; - for (T t : data) { - if(t.getClass()==type) { - data.remove(t); - modified=true; - } - } - if(modified) - onModified(); - } - public void remove(D descriptor) throws IOException { for (T t : data) { if(t.getDescriptor()==descriptor) { @@ -184,44 +115,11 @@ public class DescribableList, D extends Descriptor> } } - public void clear() { - data.clear(); - } - - public Iterator iterator() { - return data.iterator(); - } - - /** - * Called when a list is mutated. - */ - protected void onModified() throws IOException { - owner.save(); - } - @SuppressWarnings("unchecked") public Map toMap() { return (Map)Descriptor.toMap(data); } - /** - * Returns the snapshot view of instances as list. - */ - public List toList() { - return data.getView(); - } - - /** - * Gets all the {@link Describable}s in an array. - */ - public T[] toArray(T[] array) { - return data.toArray(array); - } - - public void addAllTo(Collection dst) { - data.addAllTo(dst); - } - /** * Rebuilds the list by creating a fresh instances from the submitted form. * diff --git a/core/src/main/java/hudson/util/PersistedList.java b/core/src/main/java/hudson/util/PersistedList.java new file mode 100644 index 0000000000000000000000000000000000000000..f478602dcdfd911b598d7c7c69bb17ecd4bb70e6 --- /dev/null +++ b/core/src/main/java/hudson/util/PersistedList.java @@ -0,0 +1,211 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2009, Sun Microsystems, Inc. + * + * 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.util; + +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.converters.collections.AbstractCollectionConverter; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; +import com.thoughtworks.xstream.mapper.Mapper; +import hudson.model.Describable; +import hudson.model.Saveable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +/** + * Collection whose change is notified to the parent object for persistence. + * + * @author Kohsuke Kawaguchi + * @since 1.MULTISOURCE + */ +public class PersistedList implements Iterable { + protected final CopyOnWriteList data = new CopyOnWriteList(); + protected Saveable owner; + + protected PersistedList() { + } + + public PersistedList(Saveable owner) { + setOwner(owner); + } + + public void setOwner(Saveable owner) { + this.owner = owner; + } + + public void add(T item) throws IOException { + data.add(item); + onModified(); + } + + public void addAll(Collection items) throws IOException { + data.addAll(items); + onModified(); + } + + public void replaceBy(Collection col) throws IOException { + data.replaceBy(col); + onModified(); + } + + public U get(Class type) { + for (T t : data) + if(type.isInstance(t)) + return type.cast(t); + return null; + } + + /** + * Gets all instances that matches the given type. + */ + public List getAll(Class type) { + List r = new ArrayList(); + for (T t : data) + if(type.isInstance(t)) + r.add(type.cast(t)); + return r; + } + + public int size() { + return data.size(); + } + + /** + * Removes an instance by its type. + */ + public void remove(Class type) throws IOException { + for (T t : data) { + if(t.getClass()==type) { + data.remove(t); + onModified(); + return; + } + } + } + + public boolean remove(T o) throws IOException { + boolean b = data.remove(o); + if (b) onModified(); + return b; + } + + public void removeAll(Class type) throws IOException { + boolean modified=false; + for (T t : data) { + if(t.getClass()==type) { + data.remove(t); + modified=true; + } + } + if(modified) + onModified(); + } + + + public void clear() { + data.clear(); + } + + public Iterator iterator() { + return data.iterator(); + } + + /** + * Called when a list is mutated. + */ + protected void onModified() throws IOException { + owner.save(); + } + + /** + * Returns the snapshot view of instances as list. + */ + public List toList() { + return data.getView(); + } + + /** + * Gets all the {@link Describable}s in an array. + */ + public T[] toArray(T[] array) { + return data.toArray(array); + } + + public void addAllTo(Collection dst) { + data.addAllTo(dst); + } + + public boolean isEmpty() { + return data.isEmpty(); + } + + /** + * {@link Converter} implementation for XStream. + * + * Serializaion form is compatible with plain {@link List}. + */ + public static class ConverterImpl extends AbstractCollectionConverter { + CopyOnWriteList.ConverterImpl copyOnWriteListConverter; + + public ConverterImpl(Mapper mapper) { + super(mapper); + copyOnWriteListConverter = new CopyOnWriteList.ConverterImpl(mapper()); + } + + public boolean canConvert(Class type) { + // handle subtypes in case the onModified method is overridden. + return PersistedList.class.isAssignableFrom(type); + } + + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + for (Object o : (PersistedList) source) + writeItem(o, context, writer); + } + + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + CopyOnWriteList core = copyOnWriteListConverter.unmarshal(reader, context); + + try { + PersistedList r = (PersistedList)context.getRequiredType().newInstance(); + r.data.replaceBy(core); + return r; + } catch (InstantiationException e) { + InstantiationError x = new InstantiationError(); + x.initCause(e); + throw x; + } catch (IllegalAccessException e) { + IllegalAccessError x = new IllegalAccessError(); + x.initCause(e); + throw x; + } + } + } +} + diff --git a/core/src/main/resources/hudson/PluginManager/checkUpdates.jelly b/core/src/main/resources/hudson/PluginManager/checkUpdates.jelly index b8b251568f44bc228a9713712f0b1244d24f3f4b..b46db05c6c34d65a024e9606616b16723aa5c8e8 100644 --- a/core/src/main/resources/hudson/PluginManager/checkUpdates.jelly +++ b/core/src/main/resources/hudson/PluginManager/checkUpdates.jelly @@ -38,7 +38,7 @@ THE SOFTWARE. diff --git a/core/src/main/resources/hudson/PluginManager/sites.jelly b/core/src/main/resources/hudson/PluginManager/sites.jelly new file mode 100644 index 0000000000000000000000000000000000000000..52adf178c607f4c9cfe3985a0945651fbea36a1f --- /dev/null +++ b/core/src/main/resources/hudson/PluginManager/sites.jelly @@ -0,0 +1,67 @@ + + + + + + + +

+ + + + + + +

+ This Hudson is configured to receive updates from the following sourcse: +

+ + + + + + + + + +
+ + + +
+ + + + diff --git a/core/src/main/resources/hudson/PluginManager/tabBar.jelly b/core/src/main/resources/hudson/PluginManager/tabBar.jelly index a8e6f4183d44d3dd9029fdbc6c05c49892c49849..7ad4c03b6ab10d4767a31a9d8b04518914ccfd96 100644 --- a/core/src/main/resources/hudson/PluginManager/tabBar.jelly +++ b/core/src/main/resources/hudson/PluginManager/tabBar.jelly @@ -30,6 +30,7 @@ THE SOFTWARE. + diff --git a/core/src/main/resources/hudson/PluginManager/table.jelly b/core/src/main/resources/hudson/PluginManager/table.jelly index b364680f71ccd96f0c4d149fb35b6a0c5fcceb55..33017c5582647cc9c7643bbd003deaad6427168c 100644 --- a/core/src/main/resources/hudson/PluginManager/table.jelly +++ b/core/src/main/resources/hudson/PluginManager/table.jelly @@ -45,7 +45,7 @@ THE SOFTWARE. - + +
${%compatWarning}
+ +
@@ -54,9 +54,9 @@ THE SOFTWARE.
${p.excerpt}
-
${%compatWarning}
-
-
diff --git a/core/src/main/resources/hudson/model/UpdateCenter/PageDecoratorImpl/footer.jelly b/core/src/main/resources/hudson/model/UpdateCenter/PageDecoratorImpl/footer.jelly index 3b4a4633a8620df2e31a38a9dd600635a7ffee91..2f5b00a7d9a053e254bee9425dafc10fe46c5e02 100644 --- a/core/src/main/resources/hudson/model/UpdateCenter/PageDecoratorImpl/footer.jelly +++ b/core/src/main/resources/hudson/model/UpdateCenter/PageDecoratorImpl/footer.jelly @@ -30,12 +30,18 @@ THE SOFTWARE. This file is pulled into the layout.jelly --> - - - + + + + + diff --git a/core/src/test/java/hudson/model/UpdateCenterTest.java b/core/src/test/java/hudson/model/UpdateCenterTest.java index cfbd59b4d20b9c799a08120729ad8c5675bb3a11..eac818007644870abefa1d6f822d61034a30a198 100644 --- a/core/src/test/java/hudson/model/UpdateCenterTest.java +++ b/core/src/test/java/hudson/model/UpdateCenterTest.java @@ -49,8 +49,8 @@ public class UpdateCenterTest extends TestCase { String jsonp = IOUtils.toString(url.openStream()); String json = jsonp.substring(jsonp.indexOf('(')+1,jsonp.lastIndexOf(')')); - UpdateCenter uc = new UpdateCenter(null); - UpdateCenter.Data data = uc.new Data(JSONObject.fromObject(json)); + UpdateSite us = new UpdateSite("default", url.toExternalForm()); + UpdateSite.Data data = us.new Data(JSONObject.fromObject(json)); assertTrue(data.core.url.startsWith("https://hudson.dev.java.net/")); assertTrue(data.plugins.containsKey("rake")); System.out.println(data.core.url); diff --git a/test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java b/test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java index a4b66c8dbc7bdbd77d210dff638c9c3f4cd30e05..ddbde07a8d130c23fe22b821125734a9d14e5c4b 100644 --- a/test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java +++ b/test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java @@ -53,10 +53,10 @@ import hudson.model.Run; import hudson.model.Saveable; import hudson.model.TaskListener; import hudson.model.UpdateCenter; +import hudson.model.UpdateSite; import hudson.model.AbstractProject; import hudson.model.View; import hudson.model.RootAction; -import hudson.model.UpdateCenter.UpdateCenterConfiguration; import hudson.model.Node.Mode; import hudson.security.csrf.CrumbIssuer; import hudson.slaves.CommandLauncher; @@ -238,11 +238,10 @@ public abstract class HudsonTestCase extends TestCase implements RootAction { // load updates from local proxy to avoid network traffic. final String updateCenterUrl = "http://localhost:"+JavaNetReverseProxy.getInstance().localPort+"/"; - hudson.getUpdateCenter().configure(new UpdateCenterConfiguration() { - @Override public String getUpdateCenterUrl() { - return updateCenterUrl; - } - }); + List newSites = new ArrayList(); + newSites.add(new UpdateSite("default", updateCenterUrl)); + hudson.getUpdateCenter().replaceSources(newSites); + // don't waste bandwidth talking to the update center DownloadService.neverUpdate = true; UpdateCenter.neverUpdate = true; diff --git a/war/resources/scripts/hudson-behavior.js b/war/resources/scripts/hudson-behavior.js index 3483210a9ac65273ee0f2f82a2d08428be8c8057..2e020ec20c03d0c3b535be241ad13e9df4d1e8a3 100644 --- a/war/resources/scripts/hudson-behavior.js +++ b/war/resources/scripts/hudson-behavior.js @@ -1552,6 +1552,11 @@ var downloadService = { }, post : function(id,data) { + if (data==undefined) { + // default to id in data + data = id; + id = data.id; + } var o = this.continuations[id]; new Ajax.Request(o.postBack, { parameters:{json:Object.toJSON(data)}, @@ -1563,28 +1568,8 @@ var downloadService = { } }; -// update center service. for historical reasons, -// this is separate from downloadSerivce -var updateCenter = { - postBackURL : null, - info: {}, - completionHandler: null, - url: "http://hudson-ci.org/", - - checkUpdates : function() { - loadScript(updateCenter.url+"update-center.json?"+Hash.toQueryString(updateCenter.info)); - }, - - post : function(data) { - new Ajax.Request(updateCenter.postBackURL, { - parameters:{json:Object.toJSON(data)}, - onSuccess: function() { - if(updateCenter.completionHandler!=null) - updateCenter.completionHandler(); - } - }); - } -}; +// update center service. to remain compatible with earlier version of Hudson, aliased. +var updateCenter = downloadService; /* redirects to a page once the page is ready.