From 720be7d7a12facd3543c0ffa2d2757ef76b9c06a Mon Sep 17 00:00:00 2001 From: kohsuke Date: Thu, 29 Oct 2009 21:27:46 +0000 Subject: [PATCH] Merged revisions 23183,23185,23245,23256,23266,23279-23284,23286 via svnmerge from https://www.dev.java.net/svn/hudson/branches/multiple-update-sources ........ r23183 | abayer | 2009-10-25 11:53:17 -0700 (Sun, 25 Oct 2009) | 1 line First chunk of work towards supporting multiple update centers - infrastructure in place for it, and default update center treated as only one of multiple - have to use slightly modified update-center.json though ........ r23185 | abayer | 2009-10-25 12:25:49 -0700 (Sun, 25 Oct 2009) | 1 line a number of tweaks - among other things, now works properly with additional update centers defined in hudson.model.UpdateCenter.xml - try using http://andrewbayer.com/images/hello-world/helloWorld-update-center.json as second update center ........ r23245 | abayer | 2009-10-27 13:45:25 -0700 (Tue, 27 Oct 2009) | 1 line Modified to support existing default update-center.json ........ r23256 | kohsuke | 2009-10-27 18:30:15 -0700 (Tue, 27 Oct 2009) | 7 lines - moved data binding of update-center.json to UpdateSource. - moved some of the UpdateSource property into JSON, to improve the user experience when adding an update source. the user should just type in one URL, and everything else should happen automatically. - it doesn't make sense for UpdateSource to rely on UpdateCenterConfiguration, since behaviors cannot be modified per UpdateSource basis. Instead, leaving it in UpdateCenter allows us to maintain backward compatible behaviors with the existing custom UpdateCenterConfiguration implementation. ........ r23266 | kohsuke | 2009-10-27 19:29:24 -0700 (Tue, 27 Oct 2009) | 1 line serve id inside JSON. ........ r23279 | kohsuke | 2009-10-28 10:44:00 -0700 (Wed, 28 Oct 2009) | 1 line Do not special-case "default" ID. Receive hudson.war updates from wherever that provides one. ........ r23280 | kohsuke | 2009-10-28 10:50:57 -0700 (Wed, 28 Oct 2009) | 1 line formatting change. ........ r23281 | kohsuke | 2009-10-28 11:30:40 -0700 (Wed, 28 Oct 2009) | 1 line typo ........ r23282 | kohsuke | 2009-10-28 11:45:06 -0700 (Wed, 28 Oct 2009) | 2 lines - added the UI to remove sites. - persistence of UpdateSource happens more automatically now. ........ r23283 | kohsuke | 2009-10-28 11:50:04 -0700 (Wed, 28 Oct 2009) | 1 line UpdateSource -> UpdateSite to align terminology with Eclipse. ........ r23284 | kohsuke | 2009-10-28 11:55:03 -0700 (Wed, 28 Oct 2009) | 5 lines Actually, for now, just having a programmatic modification to UpdateSite would satisfy the primary use case. And I'd like to upgrade YUI to a more recent version so that we can use its DataTable for UI. Plus the single update center model encourages the community to bring the code into the Hudson project, which is something we'd like to keep. So all in all, I'm postponing the UI work. ........ r23286 | kohsuke | 2009-10-28 13:07:19 -0700 (Wed, 28 Oct 2009) | 1 line Renaming and bug fixes. ........ git-svn-id: https://hudson.dev.java.net/svn/hudson/trunk/hudson/main@23326 71c3de6d-444a-0410-be80-ed276b4c234a --- core/src/main/java/hudson/Functions.java | 7 - core/src/main/java/hudson/PluginManager.java | 35 +- core/src/main/java/hudson/PluginWrapper.java | 13 +- core/src/main/java/hudson/XmlFile.java | 2 +- .../java/hudson/model/DownloadService.java | 2 +- core/src/main/java/hudson/model/Hudson.java | 8 +- .../main/java/hudson/model/UpdateCenter.java | 705 ++++++------------ .../main/java/hudson/model/UpdateSite.java | 558 ++++++++++++++ .../java/hudson/util/DescribableList.java | 104 +-- .../main/java/hudson/util/PersistedList.java | 211 ++++++ .../hudson/PluginManager/checkUpdates.jelly | 2 +- .../hudson/PluginManager/sites.jelly | 67 ++ .../hudson/PluginManager/tabBar.jelly | 1 + .../hudson/PluginManager/table.jelly | 8 +- .../PageDecoratorImpl/footer.jelly | 22 +- .../java/hudson/model/UpdateCenterTest.java | 4 +- .../org/jvnet/hudson/test/HudsonTestCase.java | 11 +- war/resources/scripts/hudson-behavior.js | 29 +- 18 files changed, 1142 insertions(+), 647 deletions(-) create mode 100644 core/src/main/java/hudson/model/UpdateSite.java create mode 100644 core/src/main/java/hudson/util/PersistedList.java create mode 100644 core/src/main/resources/hudson/PluginManager/sites.jelly diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index 60b23cca7f..a5bbc68493 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 d4cbe81e6f..9086bc8501 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 4a9c9a8119..4f610a0910 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 6ed07f8083..7318c16ed0 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 f5b0beaa61..edaf8614b0 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 5dd6dd42ba..60d6ecfa42 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 722b868cf3..11962f43c8 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 0000000000..e6d31442d6 --- /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 df0988024f..2597e0369a 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 0000000000..f478602dcd --- /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 b8b251568f..b46db05c6c 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 0000000000..52adf178c6 --- /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 a8e6f4183d..7ad4c03b6a 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 b364680f71..33017c5582 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 3b4a4633a8..2f5b00a7d9 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 cfbd59b4d2..eac8180076 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 a4b66c8dbc..ddbde07a8d 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 3483210a9a..2e020ec20c 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. -- GitLab