/** * ***************************************************************************** * * Copyright (c) 2012 Oracle Corporation. * * All rights reserved. This program and the accompanying materials are made * available under the terms of the Eclipse Public License v1.0 which * accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Winston Prakash * ****************************************************************************** */ package org.eclipse.hudson.plugins; import hudson.ProxyConfiguration; import hudson.Util; import hudson.lifecycle.Lifecycle; import hudson.lifecycle.RestartNotSupportedException; import hudson.markup.MarkupFormatter; import hudson.model.Hudson; import hudson.security.Permission; import hudson.util.DaemonThreadFactory; import hudson.util.VersionNumber; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.net.MalformedURLException; import java.net.URL; import java.util.*; import java.util.concurrent.*; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.io.FilenameUtils; import org.eclipse.hudson.plugins.InstalledPluginManager.Dependency; import org.eclipse.hudson.plugins.InstalledPluginManager.InstalledPluginInfo; import org.eclipse.hudson.plugins.UpdateSiteManager.AvailablePluginInfo; import org.eclipse.hudson.security.HudsonSecurityEntitiesHolder; import org.eclipse.hudson.security.HudsonSecurityManager; import org.kohsuke.stapler.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Plugin center for installing, updating and disabling plugins * * @since 3.0.0 * @author Winston Prakash */ final public class PluginCenter { private Logger logger = LoggerFactory.getLogger(PluginCenter.class); private final File pluginsDir; private final UpdateSiteManager updateSiteManager; private final InstalledPluginManager installedPluginManager; private final ProxyConfiguration proxyConfig; private List<PluginInstallationJob> installationsJobs = new CopyOnWriteArrayList<PluginInstallationJob>(); private final ExecutorService installerService = Executors.newSingleThreadExecutor( new DaemonThreadFactory(new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setName("Plugin center installer thread"); return t; } })); private final HudsonSecurityManager hudsonSecurityManager; private final File hudsonHomeDir; public PluginCenter(File homeDir) throws MalformedURLException, IOException { hudsonHomeDir = homeDir; pluginsDir = new File(hudsonHomeDir, "plugins"); if (!pluginsDir.exists()) { pluginsDir.mkdirs(); } hudsonSecurityManager = HudsonSecurityEntitiesHolder.getHudsonSecurityManager(); proxyConfig = new ProxyConfiguration(homeDir); updateSiteManager = new UpdateSiteManager("default", hudsonHomeDir, proxyConfig); installedPluginManager = new InstalledPluginManager(pluginsDir); } public boolean needsAdminLogin() { return !hudsonSecurityManager.hasPermission(Permission.HUDSON_ADMINISTER); } public ProxyConfiguration getProxyConfig() { return proxyConfig; } public UpdateSiteManager getUpdateSiteManager() { return updateSiteManager; } public List<AvailablePluginInfo> getAvailablePlugins(String pluginType) { return updateSiteManager.getAvailablePlugins(pluginType); } public List<AvailablePluginInfo> getCategorizedAvailablePlugins(String pluginType, String category) { return updateSiteManager.getCategorizedAvailablePlugins(pluginType, category); } public MarkupFormatter getMarkupFormatter() { return hudsonSecurityManager.getMarkupFormatter(); } public List<AvailablePluginInfo> getInstalledPlugins() { List<AvailablePluginInfo> installedPlugins = new ArrayList<AvailablePluginInfo>(); Set<String> installedPluginNames = installedPluginManager.getInstalledPluginNames(); for (String pluginName : installedPluginNames) { AvailablePluginInfo availablePlugin = updateSiteManager.getAvailablePlugin(pluginName); if (availablePlugin != null) { if (availablePlugin.isObsolete()) { InstalledPluginInfo installedPluginInfo = installedPluginManager.getInstalledPlugin(pluginName); if (installedPluginInfo.isEnabled()) { installedPlugins.add(availablePlugin); } } else { installedPlugins.add(availablePlugin); } } else { InstalledPluginInfo installedPluginInfo = installedPluginManager.getInstalledPlugin(pluginName); availablePlugin = updateSiteManager.createAvailablePluginInfo(installedPluginInfo.getShortName(), installedPluginInfo.getVersion(), installedPluginInfo.getLongName(), installedPluginInfo.getWikiUrl()); installedPlugins.add(availablePlugin); } } return installedPlugins; } public List<AvailablePluginInfo> getUpdatablePlugins() { List<AvailablePluginInfo> updatablePlugins = new ArrayList<AvailablePluginInfo>(); Set<String> installedPluginNames = installedPluginManager.getInstalledPluginNames(); Set<String> availablePluginNames = updateSiteManager.getAvailablePluginNames(); for (String pluginName : availablePluginNames) { AvailablePluginInfo availablePlugin = updateSiteManager.getAvailablePlugin(pluginName); if (installedPluginNames.contains(pluginName)) { InstalledPluginInfo installedPlugin = installedPluginManager.getInstalledPlugin(pluginName); if (!availablePlugin.isObsolete() && isNewerThan(availablePlugin.getVersion(), installedPlugin.getVersion())) { updatablePlugins.add(availablePlugin); } } } return updatablePlugins; } public boolean isInstalled(AvailablePluginInfo availablePlugin) { if (installedPluginManager.isInstalled(availablePlugin.getName())) { InstalledPluginInfo installedPluginInfo = installedPluginManager.getInstalledPlugin(availablePlugin.getName()); return !installedPluginInfo.isFailedToLoad(); } else { return false; } } public boolean isFailedtoLoad(AvailablePluginInfo availablePlugin) { if (installedPluginManager.isInstalled(availablePlugin.getName())) { InstalledPluginInfo installedPluginInfo = installedPluginManager.getInstalledPlugin(availablePlugin.getName()); return installedPluginInfo.isFailedToLoad(); } else { return false; } } public boolean isNewerCoreRequired(AvailablePluginInfo availablePlugin) { if (availablePlugin.getRequiredCoreVersion() != null) { return Hudson.getVersion().isOlderThan(availablePlugin.getRequiredCoreVersion()); } return false; } public boolean isUpdatable(AvailablePluginInfo availablePlugin) { Set<String> installedPluginNames = installedPluginManager.getInstalledPluginNames(); if (installedPluginNames.contains(availablePlugin.getName())) { //Installed InstalledPluginInfo installedPlugin = installedPluginManager.getInstalledPlugin(availablePlugin.getName()); return isNewerThan(availablePlugin.getVersion(), installedPlugin.getVersion()); } return false; } public InstalledPluginInfo getInstalled(AvailablePluginInfo plugin) { return installedPluginManager.getInstalledPlugin(plugin.getName()); } public Future<PluginInstallationJob> install(AvailablePluginInfo plugin) { for (AvailablePluginInfo dep : getNeededDependencies(plugin)) { install(dep); } return submitInstallationJob(plugin); } public boolean isProxyNeeded() { try { // Try opening the URL and see if the proxy works fine proxyConfig.openUrl(new URL("http://www.hudson-ci.org/")); } catch (IOException ex) { logger.debug(ex.getLocalizedMessage()); return true; } return false; } public static boolean disableUpdateCenterSwitch() { return Boolean.getBoolean("hudson.pluginManager.disableUpdateCenterSwitch"); } public HttpResponse doUpdatePlugin(@QueryParameter String pluginName) { return doInstallPlugin(pluginName); } public HttpResponse doInstallPlugin(@QueryParameter String pluginName) { if (!hudsonSecurityManager.hasPermission(Permission.HUDSON_ADMINISTER)) { return HttpResponses.forbidden(); } AvailablePluginInfo plugin = updateSiteManager.getAvailablePlugin(pluginName); if (plugin != null) { try { PluginInstallationJob installJob = null; // If the plugin is already being installed, don't schedule another. Make the search thread safe List<PluginInstallationJob> jobs = Collections.synchronizedList(installationsJobs); synchronized (jobs) { for (PluginInstallationJob job : jobs) { if (job.getName().equals(pluginName)) { installJob = job; } } } // No previous install of the plugn, create new if (installJob == null) { Future<PluginInstallationJob> newJob = install(plugin); installJob = newJob.get(); } if (!installJob.getStatus()) { installationsJobs.remove(installJob); return new ErrorHttpResponse("Plugin " + pluginName + " could not be installed. " + installJob.getErrorMsg()); } } catch (Exception ex) { return new ErrorHttpResponse("Plugin " + pluginName + " could not be installed. " + ex.getLocalizedMessage()); } installedPluginManager.loadInstalledPlugins(); return HttpResponses.ok(); } return new ErrorHttpResponse("Plugin " + pluginName + " is not a valid plugin"); } public HttpResponse doEnablePlugin(@QueryParameter String pluginName, @QueryParameter boolean enable) { if (!hudsonSecurityManager.hasPermission(Permission.HUDSON_ADMINISTER)) { return HttpResponses.forbidden(); } InstalledPluginInfo plugin = installedPluginManager.getInstalledPlugin(pluginName); try { plugin.setEnable(enable); } catch (Exception ex) { return new ErrorHttpResponse("Plugin " + pluginName + " could not be enabled/disabled. " + ex.getLocalizedMessage()); } return HttpResponses.ok(); } public HttpResponse doDowngradePlugin(@QueryParameter String pluginName) { if (!hudsonSecurityManager.hasPermission(Permission.HUDSON_ADMINISTER)) { return HttpResponses.forbidden(); } InstalledPluginInfo plugin = installedPluginManager.getInstalledPlugin(pluginName); try { plugin.downgade(); } catch (Exception ex) { return new ErrorHttpResponse("Plugin " + pluginName + " could not be reverted to previous version. " + ex.getLocalizedMessage()); } return HttpResponses.ok(); } public HttpResponse doUnpinPlugin(@QueryParameter String pluginName) { if (!hudsonSecurityManager.hasPermission(Permission.HUDSON_ADMINISTER)) { return HttpResponses.forbidden(); } InstalledPluginInfo plugin = installedPluginManager.getInstalledPlugin(pluginName); try { plugin.unpin(); } catch (Exception ex) { return new ErrorHttpResponse("Plugin " + pluginName + " could not be unpinned. " + ex.getLocalizedMessage()); } return HttpResponses.ok(); } public HttpResponse doUploadPlugin(StaplerRequest request) throws IOException, ServletException { if (!hudsonSecurityManager.hasPermission(Permission.HUDSON_ADMINISTER)) { return HttpResponses.forbidden(); } try { List<FileItem> items = new ServletFileUpload(new DiskFileItemFactory()).parseRequest(request); for (FileItem fileItem : items) { if (fileItem.getFieldName().equals("file")) { String fileName = FilenameUtils.getName(fileItem.getName()); if ("".equals(fileName) || !fileName.endsWith(".hpi")) { return new ErrorHttpResponse("File " + fileName + " may not be a plugin"); } File uploadedPluginFile = new File(pluginsDir, fileName); fileItem.write(uploadedPluginFile); InstalledPluginInfo uploadedPluginInfo = new InstalledPluginInfo(uploadedPluginFile); for (Dependency dep : uploadedPluginInfo.getDependencies()) { doInstallPlugin(dep.getShortName()); } return HttpResponses.plainText("Plugin " + fileName + " successfully uploaded."); } } } catch (Exception exc) { return new ErrorHttpResponse(exc.getLocalizedMessage()); } return new ErrorHttpResponse("Failed to upload plugin"); } public void doRestart(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { Lifecycle lifecycle = Lifecycle.get(); logger.info("Safely restarting Hudson..."); try { Hudson.getInstance().safeRestart(); } catch (RestartNotSupportedException ex) { logger.error("Restart not supported", ex); } rsp.forwardToPreviousPage(req); } public HttpResponse doSearchPlugins(@QueryParameter String searchStr, @QueryParameter boolean searchDescription) { PluginSearchList pluginSearchList = new PluginSearchList(this, searchStr, searchDescription); return HttpResponses.forwardToView(pluginSearchList, "index.jelly"); } public static class PluginSearchList { private final Set<AvailablePluginInfo> searhcedPlugins; private final PluginCenter pluginCenter; public PluginSearchList(PluginCenter pluginCenter, String searchStr, boolean searchDescription) { this.pluginCenter = pluginCenter; searhcedPlugins = pluginCenter.getUpdateSiteManager().searchPlugins(searchStr, searchDescription); } public Set<AvailablePluginInfo> getPlugins() { return searhcedPlugins; } public PluginCenter getPluginCenter() { return pluginCenter; } } private static class ErrorHttpResponse implements HttpResponse { private String message; ErrorHttpResponse(String message) { this.message = message; } @Override public void generateResponse(StaplerRequest sr, StaplerResponse rsp, Object o) throws IOException, ServletException { rsp.setStatus(HttpServletResponse.SC_BAD_REQUEST); rsp.setContentType("text/plain;charset=UTF-8"); PrintWriter w = new PrintWriter(rsp.getWriter()); w.println(message); w.close(); } } public HttpResponse doProxyConfigure( @QueryParameter("proxy.server") String server, @QueryParameter("proxy.port") String port, @QueryParameter("proxy.noProxyFor") String noProxyFor, @QueryParameter("proxy.userName") String userName, @QueryParameter("proxy.password") String password, @QueryParameter("proxy.authNeeded") String authNeeded) throws IOException { if (!hudsonSecurityManager.hasPermission(Permission.HUDSON_ADMINISTER)) { return HttpResponses.forbidden(); } try { boolean proxySet = setProxy(server, port, noProxyFor, userName, password, authNeeded); if (proxySet) { proxyConfig.save(); } // Try opening a URL and see if the proxy works fine proxyConfig.openUrl(new URL("http://www.hudson-ci.org/")); } catch (IOException ex) { return HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST, ex); } return HttpResponses.ok(); } private boolean setProxy(String server, String port, String noProxyFor, String userName, String password, String authNeeded) throws IOException { server = Util.fixEmptyAndTrim(server); if ((server != null) && !"".equals(server)) { // If port is not specified assume it is port 80 (usual default for HTTP port) int portNumber = 80; if (!"".equals(Util.fixNull(port))) { portNumber = Integer.parseInt(Util.fixNull(port)); } boolean proxyAuthNeeded = "on".equals(Util.fixNull(authNeeded)); if (!proxyAuthNeeded) { userName = ""; password = ""; } proxyConfig.configure(server, portNumber, Util.fixEmptyAndTrim(noProxyFor), Util.fixEmptyAndTrim(userName), Util.fixEmptyAndTrim(password), "on".equals(Util.fixNull(authNeeded))); return true; } else { proxyConfig.getXmlFile().delete(); proxyConfig.configure(null, -1, null, null, null, false); return false; } } public HttpResponse doConfigureUpdateSite(@QueryParameter String siteUrl) throws IOException { if (!hudsonSecurityManager.hasPermission(Permission.HUDSON_ADMINISTER)) { return HttpResponses.forbidden(); } try { updateSiteManager.verifyUpdateSite(siteUrl); updateSiteManager.setUpdateSiteUrl(siteUrl); // Ok a valid update site URL set it to the plugin manager. // For now let Plugin Manager periodically update the local cache. Hudson.getInstance().getPluginManager().doSiteConfigure(siteUrl); } catch (IOException ex) { return new ErrorHttpResponse("Update Site Could not be set. " + ex.getLocalizedMessage()); } return HttpResponses.ok(); } public HttpResponse doRefreshUpdateCenter() throws IOException { if (!hudsonSecurityManager.hasPermission(Permission.HUDSON_ADMINISTER)) { return HttpResponses.forbidden(); } // Check if you are able to connect try { URL updateCenterRemoteUrl = new URL(updateSiteManager.getUpdateSiteUrl()); proxyConfig.openUrl(updateCenterRemoteUrl); } catch (Exception exc) { return new ErrorHttpResponse("Could not connect to " + updateSiteManager.getUpdateSiteUrl() + ". " + "If you are behind a firewall set HTTP proxy and try again."); } try { updateSiteManager.refreshFromUpdateSite(); } catch (IOException ex) { return new ErrorHttpResponse("Updates could not be refreshed. " + ex.getLocalizedMessage()); } return HttpResponses.ok(); } private Future<PluginInstallationJob> submitInstallationJob(AvailablePluginInfo plugin) { PluginInstallationJob newJob = new PluginInstallationJob(plugin, pluginsDir, proxyConfig); installationsJobs.add(newJob); return installerService.submit(newJob, newJob); } public void shutdown() { List<Runnable> running = installerService.shutdownNow(); if (!running.isEmpty()) { logger.warn("shutdown with " + running.size() + " jobs pending"); } } private boolean isNewerThan(String availableVersion, String installedVersion) { try { return new VersionNumber(installedVersion).compareTo(new VersionNumber(availableVersion)) < 0; } catch (IllegalArgumentException e) { // couldn't parse as the version number. return false; } } private List<AvailablePluginInfo> getNeededDependencies(AvailablePluginInfo pluginInfo) { List<AvailablePluginInfo> deps = new ArrayList<AvailablePluginInfo>(); if ((pluginInfo != null) && (pluginInfo.getDependencies().size() > 0)) { for (Map.Entry<String, String> e : pluginInfo.getDependencies().entrySet()) { AvailablePluginInfo depPlugin = updateSiteManager.getAvailablePlugin(e.getKey()); if (depPlugin != null) { VersionNumber requiredVersion = new VersionNumber(e.getValue()); // Is the plugin installed already? If not, add it. InstalledPluginInfo current = installedPluginManager.getInstalledPlugin(depPlugin.getName()); if (current == null) { deps.add(depPlugin); } else if (current.isOlderThan(requiredVersion)) { deps.add(depPlugin); } } else { logger.error("Could not find " + e.getKey() + " which is required by " + pluginInfo.getDisplayName()); } } } return deps; } public String getLastUpdatedString() { return Hudson.getInstance().getUpdateCenter().getLastUpdatedString(); } }