提交 720be7d7 编写于 作者: K kohsuke

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
上级 938cb6ba
......@@ -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.
*
......
......@@ -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;
......
......@@ -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());
}
......
......@@ -140,7 +140,7 @@ public final class XmlFile {
*
* @return
* The unmarshalled object. Usually the same as <tt>o</tt>, 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"));
......
......@@ -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;
}
/**
......
......@@ -490,7 +490,7 @@ public final class Hudson extends Node implements ItemGroup<TopLevelItem>, 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<TopLevelItem>, 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<TopLevelItem>, 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<TopLevelItem>, 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
......
......@@ -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?
*
* <p>
* 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<UpdateCenterJob> jobs = new Vector<UpdateCenterJob>();
/**
* 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<UpdateSite> sourcesUsed = new HashSet<UpdateSite>();
/**
* 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<UpdateSite> sites = new PersistedList<UpdateSite>(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<UpdateSite> 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<X509Certificate> certs = new ArrayList<X509Certificate>();
{// 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<TrustAnchor> anchors = CertificateUtil.getDefaultRootCAs();
ServletContext context = Hudson.getInstance().servletContext;
for (String cert : (Set<String>) 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 <tt>hudson.war</tt>.
*
* @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<UpdateCenterJob> addJob(UpdateCenterJob job) {
/*package*/ synchronized Future<UpdateCenterJob> 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<Plugin> getAvailables() {
List<Plugin> r = new ArrayList<Plugin>();
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<Plugin> getUpdates() {
Data data = getData();
if(data==null) return Collections.emptyList(); // fail to determine
public List<Plugin> getAvailables() {
List<Plugin> plugins = new ArrayList<Plugin>();
List<Plugin> r = new ArrayList<Plugin>();
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<Plugin> getUpdates() {
List<Plugin> plugins = new ArrayList<Plugin>();
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.
*
* <p>
* 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}.
*
* <p>
* 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<String,Plugin> plugins = new TreeMap<String,Plugin>(String.CASE_INSENSITIVE_ORDER);
Data(JSONObject o) {
core = new Entry(o.getJSONObject("core"));
for(Map.Entry<String,JSONObject> e : (Set<Map.Entry<String,JSONObject>>)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.
*
* <p>
* 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.
*
* <p>
* This is mainly intended to be called from the UI. The actual installation work happens
* asynchronously in another thread.
*/
public Future<UpdateCenterJob> 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 <tt>update-center.json</tt> file. See
* <tt>http://hudson-ci.org/update-center.json</tt> 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
* <tt>update-center.json</tt> 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 <tt>row.jelly</tt> 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<String> statuses= new Vector<String>();
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);
}
}
/*
* 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"
*
* <p>
* 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?
*
* <p>
* 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 <tt>update-center.json</tt>, like <tt>http://hudson-ci.org/update-center.json</tt>.
*/
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<X509Certificate> certs = new ArrayList<X509Certificate>();
{// 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<TrustAnchor> anchors = CertificateUtil.getDefaultRootCAs();
ServletContext context = Hudson.getInstance().servletContext;
for (String cert : (Set<String>) 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<Plugin> getAvailables() {
List<Plugin> r = new ArrayList<Plugin>();
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<Plugin> getUpdates() {
Data data = getData();
if(data==null) return Collections.emptyList(); // fail to determine
List<Plugin> r = new ArrayList<Plugin>();
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<String,Plugin> plugins = new TreeMap<String,Plugin>(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<String,JSONObject> e : (Set<Map.Entry<String,JSONObject>>)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.
*
* <p>
* 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.
*
* <p>
* This is mainly intended to be called from the UI. The actual installation work happens
* asynchronously in another thread.
*/
public Future<UpdateCenterJob> 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");
}
......@@ -61,10 +61,7 @@ import java.util.Map;
*
* @author Kohsuke Kawaguchi
*/
public class DescribableList<T extends Describable<T>, D extends Descriptor<T>> implements Iterable<T> {
private final CopyOnWriteList<T> data = new CopyOnWriteList<T>();
private Saveable owner;
public class DescribableList<T extends Describable<T>, D extends Descriptor<T>> extends PersistedList<T> {
protected DescribableList() {
}
......@@ -88,20 +85,6 @@ public class DescribableList<T extends Describable<T>, D extends Descriptor<T>>
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<? extends T> 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<T extends Describable<T>, D extends Descriptor<T>>
onModified();
}
public void replaceBy(Collection<? extends T> 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<T extends Describable<T>, D extends Descriptor<T>>
return null;
}
public <U extends T> U get(Class<U> type) {
for (T t : data)
if(type.isInstance(t))
return type.cast(t);
return null;
}
/**
* Gets all instances that matches the given type.
*/
public <U extends T> List<U> getAll(Class<U> type) {
List<U> r = new ArrayList<U>();
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<? extends T> type) throws IOException {
for (T t : data) {
if(t.getClass()==type) {
data.remove(t);
onModified();
return;
}
}
}
public void removeAll(Class<? extends T> 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<T extends Describable<T>, D extends Descriptor<T>>
}
}
public void clear() {
data.clear();
}
public Iterator<T> iterator() {
return data.iterator();
}
/**
* Called when a list is mutated.
*/
protected void onModified() throws IOException {
owner.save();
}
@SuppressWarnings("unchecked")
public Map<D,T> toMap() {
return (Map)Descriptor.toMap(data);
}
/**
* Returns the snapshot view of instances as list.
*/
public List<T> 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<? super T> dst) {
data.addAllTo(dst);
}
/**
* Rebuilds the list by creating a fresh instances from the submitted form.
*
......
/*
* 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<T> implements Iterable<T> {
protected final CopyOnWriteList<T> data = new CopyOnWriteList<T>();
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<? extends T> items) throws IOException {
data.addAll(items);
onModified();
}
public void replaceBy(Collection<? extends T> col) throws IOException {
data.replaceBy(col);
onModified();
}
public <U extends T> U get(Class<U> type) {
for (T t : data)
if(type.isInstance(t))
return type.cast(t);
return null;
}
/**
* Gets all instances that matches the given type.
*/
public <U extends T> List<U> getAll(Class<U> type) {
List<U> r = new ArrayList<U>();
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<? extends T> 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<? extends T> 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<T> 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<T> 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<? super T> 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;
}
}
}
}
......@@ -38,7 +38,7 @@ THE SOFTWARE.
</tr>
</table>
<script>
updateCenter.completionHandler = function() {
downloadService.completionHandler = function() {
$$('completionMarker').innerHTML = "${%Done}";
}
</script>
......
<!--
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.
-->
<!--
Add/remove update center sites
-->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<l:layout title="Update Center" permission="${app.ADMINISTER}" norefresh="true">
<st:include page="sidepanel.jelly"/>
<l:main-panel>
<form method="post" action="updateSources">
<local:tabBar page="sites" xmlns:local="/hudson/PluginManager">
<tr style="border-top: 0px;">
<th colspan="2"/>
</tr>
<tr style="border-bottom:none">
<td colspan="2">
<p>
This Hudson is configured to receive updates from the following sourcse:
</p>
</td>
</tr>
<tr style="border-top: none">
<td>
<select multiple="true" style="width:100%" name="sources">
<j:forEach var="s" items="${app.updateCenter.sources}">
<option value="${s.id}">${s.url}</option>
</j:forEach>
</select>
</td>
<td id="buttonBar" style="width:13em;">
<style>
#buttonBar INPUT {
width: 10em;
}
</style>
<input type="submit" value="${%Add...}" name="add" />
<br />
<input type="submit" value="${%Remove}" name="remove" disabled="${empty(app.updateCenter.sources)?'true':null}"/>
</td>
</tr>
</local:tabBar>
</form>
</l:main-panel>
</l:layout>
</j:jelly>
......@@ -30,6 +30,7 @@ THE SOFTWARE.
<l:tab name="${%Updates}" active="${page=='updates'}" href="." />
<l:tab name="${%Available}" active="${page=='available'}" href="./available" />
<l:tab name="${%Installed}" active="${page=='installed'}" href="./installed" />
<!--<l:tab name="${%Sites}" active="${page=='sites'}" href="./sites" />-->
<l:tab name="${%Advanced}" active="${page=='advanced'}" href="./advanced" />
</l:tabBar>
<table id="plugins" class="sortable pane bigtable" style="margin-top:0px; border-top: none;">
......
......@@ -45,7 +45,7 @@ THE SOFTWARE.
<j:when test="${!empty(list)}">
<j:forEach var="p" items="${list}">
<tr>
<td class="pane" align="center"><input type="checkbox" name="plugin.${p.name}"/></td>
<td class="pane" align="center"><input type="checkbox" name="plugin.${p.name}.${p.sourceId}"/></td>
<td class="pane">
<div>
<a href="${p.wiki}"><st:out value="${p.displayName}"/></a>
......@@ -54,9 +54,9 @@ THE SOFTWARE.
<div class="excerpt">${p.excerpt}</div>
</j:if>
<j:if test="${!p.isCompatibleWithInstalledVersion()}">
<div class="compatWarning">${%compatWarning}</div>
</j:if>
</td>
<div class="compatWarning">${%compatWarning}</div>
</j:if>
</td>
<td class="pane"><st:out value="${p.version}" /></td>
<j:if test="${attrs.page=='updates'}">
<td>
......
......@@ -30,12 +30,18 @@ THE SOFTWARE.
This file is pulled into the layout.jelly
-->
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<j:if test="${app.updateCenter.due or forcedUpdateCheck}">
<script>
updateCenter.postBackURL = "${rootURL}/updateCenter/postBack";
updateCenter.info = { version:"${h.version}" };
updateCenter.url = "${h.updateCenterUrl}";
Behaviour.addLoadEvent(updateCenter.checkUpdates);
</script>
</j:if>
<j:forEach var="site" items="${app.updateCenter.sites}">
<j:if test="${site.due or forcedUpdateCheck}">
<script>
Behaviour.addLoadEvent(function() {
downloadService.download(
"${site.id}",
"${site.url}",
{version:"${h.version}"},
"${rootURL}/updateCenter/byId/${site.id}/postBack",
null);
});
</script>
</j:if>
</j:forEach>
</j:jelly>
......@@ -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);
......
......@@ -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<UpdateSite> newSites = new ArrayList<UpdateSite>();
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;
......
......@@ -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.
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册