Index: main/core/src/main/java/hudson/model/UpdateCenter.java =================================================================== --- main/core/src/main/java/hudson/model/UpdateCenter.java (revision 13657) +++ main/core/src/main/java/hudson/model/UpdateCenter.java (working copy) @@ -1,5 +1,6 @@ package hudson.model; +import hudson.ExtensionPoint; import hudson.Functions; import hudson.PluginManager; import hudson.PluginWrapper; @@ -11,6 +12,8 @@ import hudson.util.VersionNumber; import static hudson.util.TimeUnit2.DAYS; import net.sf.json.JSONObject; + +import org.acegisecurity.Authentication; import org.apache.commons.io.input.CountingInputStream; import org.apache.commons.io.IOUtils; import org.kohsuke.stapler.DataBoundConstructor; @@ -41,6 +44,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Controls update center capability. @@ -48,7 +53,12 @@ *

* The main job of this class is to keep track of the latest update center metadata file, and perform installations. * Much of the UI about choosing plugins to install is done in {@link PluginManager}. - * + *

+ * The update center can be configured to contact alternate servers for updates + * and plugins, and to use alternate strategies for downloading, installing + * and updating components. See the Javadocs for {@link UpdateCenterConfigurator} + * for more information. + * * @author Kohsuke Kawaguchi * @since 1.220 */ @@ -86,6 +96,104 @@ private final Vector jobs = new Vector(); /** + * Update center configuration data + */ + private UpdateCenterConfigurator config; + + /** + * Default configuration + */ + private static final UpdateCenterConfigurator DEFAULT_CONFIG = new UpdateCenterConfigurator( + new ConnectionCheckStrategy() { + public void checkConnection(ConnectionCheckJob job, String connectionCheckUrl) throws IOException { + testConnection(new URL(connectionCheckUrl + "?uctest")); + } + + public void checkUpdateCenter(ConnectionCheckJob job, String updateCenterUrl) throws IOException { + testConnection(new URL(updateCenterUrl)); + } + + private void testConnection(URL url) throws IOException { + InputStream in = ProxyConfiguration.open(url).getInputStream(); + IOUtils.copy(in,new ByteArrayOutputStream()); + in.close(); + } + }, + new ValidationStrategy() { + public void preValidate(DownloadJob job, URL src) throws IOException { + if(!src.toExternalForm().startsWith(job.configuration.getPluginRepositoryBaseUrl())) { + throw new IOException("Installation of plugin from "+src+" is not allowed"); + } + } + + public void postValidate(DownloadJob job, File src) throws IOException { + } + }, + new DownloadStrategy() { + public File download(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. + URLConnection con = ProxyConfiguration.open(src); + int total = con.getContentLength(); + CountingInputStream in = new CountingInputStream(con.getInputStream()); + byte[] buf = new byte[8192]; + int len; + + File dst = job.getDestination(); + File tmp = new File(dst.getPath()+".tmp"); + OutputStream out = new FileOutputStream(tmp); + + LOGGER.info("Downloading "+job.getName()); + while((len=in.read(buf))>=0) { + out.write(buf,0,len); + job.status = job.new Installing(total==-1 ? -1 : in.getCount()*100/total); + } + + in.close(); + out.close(); + + return tmp; + } + + }, + new InstallStrategy() { + public void install(DownloadJob job, File src, File dst) throws IOException { + job.replace(dst, src); + } + + }, + new UpgradeStrategy() { + public void upgrade(DownloadJob job, File src, File dst) throws IOException { + job.replace(dst, src); + } + }, + "http://www.google.com", + "https://hudson.dev.java.net/", + "https://hudson.dev.java.net/" + ); + + /** + * Create update center to get plugins/updates from hudson.dev.java.net + */ + public UpdateCenter() { + configure(DEFAULT_CONFIG); + } + + /** + * 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 UpdateCenterConfigurator + */ + public void configure(UpdateCenterConfigurator config) { + if (config!=null) { + this.config = config; + } + } + + /** * Returns true if it's time for us to check for new version. */ public boolean isDue() { @@ -142,7 +250,7 @@ public void doUpgrade(StaplerResponse rsp) throws IOException, ServletException { requirePOST(); Hudson.getInstance().checkPermission(Hudson.ADMINISTER); - HudsonUpgradeJob job = new HudsonUpgradeJob(); + HudsonUpgradeJob job = new HudsonUpgradeJob(Hudson.getAuthentication()); if(!Lifecycle.get().canRewriteHudsonWar()) { sendError("Hudson upgrade not supported in this running mode"); return; @@ -258,6 +366,14 @@ } /** + * Exposed to get rid of hardcoding of the URL that serves up update-center.json + * in Javascript. + */ + public String getUrl() { + return config.getUpdateCenterUrl(); + } + + /** * In-memory representation of the update center data. */ public final class Data { @@ -387,7 +503,7 @@ */ public void install() { Hudson.getInstance().checkPermission(Hudson.ADMINISTER); - addJob(new InstallationJob(this)); + addJob(new InstallationJob(this, Hudson.getAuthentication())); } /** @@ -400,6 +516,226 @@ } /** + * Configuration data for control 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.dev.java.net; and will install plugins with file system + * operations. This class, in conjunction with the {@link ConnectionCheckStrategy}, + * {@link DownloadStrategy}, {@link InstallStrategy}, and {@link UpgradeStrategy} + * interfaces provides hooks to customize any of these behaviors. + * + * @author Dean Yu + * @since 1.265 + */ + public static final class UpdateCenterConfigurator { + /** + * Create an immutable update center configuration. Null may be passed + * in for any parameter to use Hudson's default for that behavior or value. + * + * @param c A {@link ConnectionCheckStrategy} implementation. If not null, + * Hudson will use this strategy for checking network connectivity + * before trying to download a plugin or upgrade. If null, Hudson's + * default behavior will try to connect to {@link #connectionCheckUrl} + * followed by a connection to {@link #updateCenterUrl}. + * @param v A {@link ValidationStrategy} implementation. If not null, Hudson + * will use this strategy to validate a resource before downloading it, + * and to validate the downloaded resource before installing it. If + * null, Hudson's default behavior is to enforce that the URL of + * the resource starts with {@link #pluginRepositoryBaseUrl} before + * the resource is downloaded. + * @param d A {@link DownloadStrategy} implementation. If not null, Hudson + * will use this strategy to download a plugin or core upgrade. The + * download strategy Hudson uses by default will download the + * resource from the URL specified in the catalog. Regardless of + * the strategy, Hudson will always enforce that the URL starts + * with the value of {@link #pluginRepositoryBaseUrl}. + * @param i A {@link InstallStrategy} implementation. If not null, Hudson + * will use this strategy to move the downloaded file into its + * final destination. If null, Hudson will simply rename the + * downloaded file. + * @param u A {@link UpgradeStrategy} implementation. If not null, Hudson + * will use this strategy to upgrade itself. + * @param connectionCheckUrl A string containing an "always up" URL. If + * null is passed, Hudson will use "http://www.google.com". + * @param updateCenterUrl A string containing the URL to retrieve the update-center.json + * file from. If null, Hudson will use "https://hudson.dev.java.net/". + * @param pluginRepositoryBaseUrl A string containing the base URL for + * plugin downloads. Hudson enforces that all plugins must be downloaded + * from this domain. If null, Hudson will use "https://hudson.dev.java.net/". + */ + public UpdateCenterConfigurator(ConnectionCheckStrategy c, ValidationStrategy v, + DownloadStrategy d, InstallStrategy i,UpgradeStrategy u, + String connectionCheckUrl, String updateCenterUrl, String pluginRepositoryBaseUrl) { + connectionChecker = c==null ? UpdateCenter.DEFAULT_CONFIG.getConnectionCheckStrategy() : c; + validator = v==null ? UpdateCenter.DEFAULT_CONFIG.getValidationStrategy() : v; + downloader = d==null ? UpdateCenter.DEFAULT_CONFIG.getDownloadStrategy() : d; + installer = i==null ? UpdateCenter.DEFAULT_CONFIG.getInstallStrategy() : i; + upgrader = u==null ? UpdateCenter.DEFAULT_CONFIG.getUpgradeStrategy() : u; + this.connectionCheckUrl = Util.fixEmptyAndTrim(connectionCheckUrl)==null ? UpdateCenter.DEFAULT_CONFIG.getConnectionCheckUrl() : connectionCheckUrl; + this.updateCenterUrl = Util.fixEmptyAndTrim(updateCenterUrl)==null ? UpdateCenter.DEFAULT_CONFIG.getUpdateCenterUrl() : updateCenterUrl; + this.pluginRepositoryBaseUrl = Util.fixEmptyAndTrim(pluginRepositoryBaseUrl)==null ? UpdateCenter.DEFAULT_CONFIG.getPluginRepositoryBaseUrl() : pluginRepositoryBaseUrl; + } + + public ConnectionCheckStrategy getConnectionCheckStrategy() { + return connectionChecker; + } + + public ValidationStrategy getValidationStrategy() { + return validator; + } + + public DownloadStrategy getDownloadStrategy() { + return downloader; + } + + public InstallStrategy getInstallStrategy() { + return installer; + } + + public UpgradeStrategy getUpgradeStrategy() { + return upgrader; + } + + public String getConnectionCheckUrl() { + return connectionCheckUrl; + } + + public String getUpdateCenterUrl() { + return updateCenterUrl; + } + + public String getPluginRepositoryBaseUrl() { + return pluginRepositoryBaseUrl; + } + + private ConnectionCheckStrategy connectionChecker; + private ValidationStrategy validator; + private DownloadStrategy downloader; + private InstallStrategy installer; + private UpgradeStrategy upgrader; + private String connectionCheckUrl; + private String updateCenterUrl; + private String pluginRepositoryBaseUrl; + } + + /** + * Determine network connectivity before attempting to download. You will typically only + * ever need to implement this interface if you plan to fetch plugins + * over a transport protocol other than HTTP. + */ + public interface ConnectionCheckStrategy extends 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. + * @throws IOException if a connection can't be established + */ + public void checkConnection(ConnectionCheckJob job, String connectionCheckUrl) throws IOException; + + /** + * 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. + */ + public void checkUpdateCenter(ConnectionCheckJob job, String updateCenterUrl) throws IOException; + } + + /** + * Hooks for validating a resource before and after it is downloaded. This + * interface defines two validation points. Prevalidation allows validation + * of the URI (URL) a plugin or upgrade resource will be downloaded from. + * Post-validation allows validation of the resource after it has been + * downloaded. The default implementation enforces that the resource is + * hosted by {@link UpdateCenterConfigurator#pluginRepositoryBaseUrl} before + * it is downloaded. It does no additional validation of the downloaded + * bits. + */ + public interface ValidationStrategy extends ExtensionPoint { + /** + * 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; + + /** + * Validate the resource after it has been downloaded, before it is + * installed. + * + * @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. + * @throws IOException if the validation fails. + */ + public void postValidate(DownloadJob job, File src) throws IOException; + } + + /** + * Retrieve a plugin or core upgrade to a temporary location. Implement this interface if you + * need to do more than read a byte stream from an Internet host. The download + * job will not call this strategy if the resource does not originate from + * the host specified by {@link UpdateCenterConfigurator#pluginRepositoryBaseUrl}. + */ + public interface DownloadStrategy extends ExtensionPoint { + /** + * 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. + * @return A File object that describes the downloaded resource. + * @throws IOException if there were problems downloading the resource. + * @see DownloadJob + */ + public File download(DownloadJob job, URL src) throws IOException; + } + + /** + * Installs the downloaded plugin into its final location. Implement this interface + * if you need to do more than move the temporary file into place. + */ + public interface InstallStrategy extends ExtensionPoint { + /** + * Called after a plugin has been downloaded to move it into its final + * location. + * + * @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. + * @throws IOException if there are problems installing the resource. + */ + public void install(DownloadJob job, File src, File dst) throws IOException; + } + + /** + * Installs the downloaded upgrade into its final location. Implement this + * interface if you need to do more than move the temporary file into place. + */ + public interface UpgradeStrategy extends ExtensionPoint { + /** + * Called after an upgrade has been downloaded to move it into its final + * location. + * + * @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. + * @throws IOException if there are problems installing the resource. + */ + public void upgrade(DownloadJob job, File src, File dst) throws IOException; + } + + /** * Things that {@link UpdateCenter#installerService} executes. * * This object will have the row.jelly which renders the job on UI. @@ -419,19 +755,21 @@ public void run() { try { + String connectionCheckUrl = config.getConnectionCheckUrl(); + statuses.add(Messages.UpdateCenter_Status_CheckingInternet()); try { - testConnection(new URL("http://www.google.com/")); + config.getConnectionCheckStrategy().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("www.google.com")); + statuses.add(Messages.UpdateCenter_Status_ConnectionFailed(connectionCheckUrl)); return; } } statuses.add(Messages.UpdateCenter_Status_CheckingJavaNet()); - testConnection(new URL("https://hudson.dev.java.net/?uctest")); + config.getConnectionCheckStrategy().checkUpdateCenter(this, config.getUpdateCenterUrl()); statuses.add(Messages.UpdateCenter_Status_Success()); } catch (UnknownHostException e) { @@ -451,12 +789,6 @@ return statuses.toArray(new String[statuses.size()]); } } - - private void testConnection(URL url) throws IOException { - InputStream in = ProxyConfiguration.open(url).getInputStream(); - IOUtils.copy(in,new ByteArrayOutputStream()); - in.close(); - } } /** @@ -489,41 +821,40 @@ */ protected abstract void onSuccess(); + /** + * So custom strategies have access to the configuration data + */ + public final UpdateCenterConfigurator configuration = config; + + private Authentication authentication; + + /** + * Get the user that initiated this job + */ + public Authentication getUser() + { + return this.authentication; + } + + protected DownloadJob(Authentication authentication) + { + this.authentication = authentication; + } + public void run() { try { - LOGGER.info("Starting the installation of "+getName()); + LOGGER.info("Starting the installation of "+getName()+" on behalf of "+getUser().getName()); URL src = getURL(); - // for security reasons, only install from hudson.dev.java.net for now, which is also conveniently - // https to guarantee transport level security. - if(!src.toExternalForm().startsWith("https://hudson.dev.java.net/")) { - throw new IOException("Installation from non-official repository at "+src+" is not support yet"); - } + config.getValidationStrategy().preValidate(this, src); - // 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. - URLConnection con = ProxyConfiguration.open(src); - int total = con.getContentLength(); - CountingInputStream in = new CountingInputStream(con.getInputStream()); - byte[] buf = new byte[8192]; - int len; - File dst = getDestination(); - File tmp = new File(dst.getPath()+".tmp"); - OutputStream out = new FileOutputStream(tmp); - - LOGGER.info("Downloading "+getName()); - while((len=in.read(buf))>=0) { - out.write(buf,0,len); - status = new Installing(total==-1 ? -1 : in.getCount()*100/total); - } - - in.close(); - out.close(); - - replace(dst, tmp); - + File tmp = config.getDownloadStrategy().download(this, src); + + config.getValidationStrategy().postValidate(this, tmp); + config.getInstallStrategy().install(this, tmp, dst); + LOGGER.info("Installation successful: "+getName()); status = new Success(); onSuccess(); @@ -606,7 +937,8 @@ private final PluginManager pm = Hudson.getInstance().getPluginManager(); - public InstallationJob(Plugin plugin) { + public InstallationJob(Plugin plugin, Authentication auth) { + super(auth); this.plugin = plugin; } @@ -632,7 +964,8 @@ * Represents the state of the upgrade activity of Hudson core. */ public final class HudsonUpgradeJob extends DownloadJob { - public HudsonUpgradeJob() { + public HudsonUpgradeJob(Authentication auth) { + super(auth); } protected URL getURL() throws MalformedURLException { Index: main/core/src/main/resources/hudson/model/UpdateCenter/PageDecoratorImpl/footer.jelly =================================================================== --- main/core/src/main/resources/hudson/model/UpdateCenter/PageDecoratorImpl/footer.jelly (revision 13657) +++ main/core/src/main/resources/hudson/model/UpdateCenter/PageDecoratorImpl/footer.jelly (working copy) @@ -10,7 +10,8 @@ - \ No newline at end of file + Index: main/war/resources/scripts/hudson-behavior.js =================================================================== --- main/war/resources/scripts/hudson-behavior.js (revision 13657) +++ main/war/resources/scripts/hudson-behavior.js (working copy) @@ -1319,9 +1319,10 @@ postBackURL : null, info: {}, completionHandler: null, + url: "https://hudson.dev.java.net/", checkUpdates : function() { - loadScript("https://hudson.dev.java.net/update-center.json?"+Hash.toQueryString(updateCenter.info)); + loadScript(updateCenter.url+"update-center.json?"+Hash.toQueryString(updateCenter.info)); }, post : function(data) { @@ -1369,4 +1370,4 @@ } }); }, 5000); -} \ No newline at end of file +}