package hudson.model; import com.thoughtworks.xstream.XStream; import hudson.BulkChange; import hudson.FeedAdapter; import hudson.FilePath; import hudson.Functions; import hudson.Launcher; import hudson.Launcher.LocalLauncher; import hudson.Plugin; import hudson.PluginManager; import hudson.PluginWrapper; import hudson.ProxyConfiguration; import hudson.StructuredForm; import hudson.TcpSlaveAgentListener; import hudson.Util; import static hudson.Util.fixEmpty; import hudson.WebAppMain; import hudson.XmlFile; import hudson.lifecycle.WindowsInstallerLink; import hudson.lifecycle.Lifecycle; import hudson.model.Descriptor.FormException; import hudson.model.listeners.ItemListener; import hudson.model.listeners.JobListener; import hudson.model.listeners.JobListener.JobListenerAdapter; import hudson.model.listeners.SCMListener; import hudson.remoting.LocalChannel; import hudson.remoting.VirtualChannel; import hudson.scm.CVSSCM; import hudson.scm.RepositoryBrowser; import hudson.scm.RepositoryBrowsers; import hudson.scm.SCM; import hudson.scm.SCMDescriptor; import hudson.scm.SCMS; import hudson.scm.SubversionSCM; import hudson.search.CollectionSearchIndex; import hudson.search.SearchIndexBuilder; import hudson.security.ACL; import hudson.security.AccessControlled; import hudson.security.AuthorizationStrategy; import hudson.security.BasicAuthenticationFilter; import hudson.security.HudsonFilter; import hudson.security.LegacyAuthorizationStrategy; import hudson.security.LegacySecurityRealm; import hudson.security.Permission; import hudson.security.PermissionGroup; import hudson.security.SecurityMode; import hudson.security.SecurityRealm; import hudson.security.SecurityRealm.SecurityComponents; import hudson.security.TokenBasedRememberMeServices2; import hudson.slaves.ComputerListener; import hudson.slaves.RetentionStrategy; import hudson.tasks.BuildStep; import hudson.tasks.BuildWrapper; import hudson.tasks.BuildWrappers; import hudson.tasks.Builder; import hudson.tasks.DynamicLabeler; import hudson.tasks.LabelFinder; import hudson.tasks.Mailer; import hudson.tasks.Publisher; import hudson.triggers.Trigger; import hudson.triggers.TriggerDescriptor; import hudson.triggers.Triggers; import hudson.util.CaseInsensitiveComparator; import hudson.util.ClockDifference; import hudson.util.CopyOnWriteList; import hudson.util.CopyOnWriteMap; import hudson.util.DaemonThreadFactory; import hudson.util.FormFieldValidator; import hudson.util.HudsonIsLoading; import hudson.util.MultipartFormDataParser; import hudson.util.RemotingDiagnostics; import hudson.util.TextFile; import hudson.util.XStream2; import hudson.util.IncompatibleServletVersionDetected; import hudson.util.HudsonIsRestarting; import hudson.widgets.Widget; import net.sf.json.JSONObject; import org.acegisecurity.AccessDeniedException; import org.acegisecurity.Authentication; import org.acegisecurity.GrantedAuthority; import org.acegisecurity.GrantedAuthorityImpl; import org.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; import org.acegisecurity.ui.AbstractProcessingFilter; import static org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY; import org.apache.commons.logging.LogFactory; import org.kohsuke.stapler.MetaClass; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerProxy; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import javax.servlet.http.HttpSession; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.net.URL; import java.security.SecureRandom; import java.text.NumberFormat; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.Stack; import java.util.StringTokenizer; import java.util.Timer; import java.util.TreeSet; import java.util.Vector; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.regex.Pattern; import java.nio.charset.Charset; /** * Root object of the system. * * @author Kohsuke Kawaguchi */ public final class Hudson extends View implements ItemGroup, Node, StaplerProxy { private transient final Queue queue; /** * {@link Computer}s in this Hudson system. Read-only. */ private transient final Map computers = new CopyOnWriteMap.Hash(); /** * Number of executors of the master node. */ private int numExecutors = 2; /** * Job allocation strategy. */ private Mode mode = Mode.NORMAL; /** * False to enable anyone to do anything. * Left as a field so that we can still read old data that uses this flag. * * @see #authorizationStrategy * @see #securityRealm */ private Boolean useSecurity; /** * Controls how the * authorization * is handled in Hudson. *

* This ultimately controls who has access to what. * * Never null. */ private volatile AuthorizationStrategy authorizationStrategy; /** * Controls a part of the * authentication * handling in Hudson. *

* Intuitively, this corresponds to the user database. * * See {@link HudsonFilter} for the concrete authentication protocol. * * Never null. Always use {@link #setSecurityRealm(SecurityRealm)} to * update this field. * * @see #getSecurity() * @see #setSecurityRealm(SecurityRealm) */ private volatile SecurityRealm securityRealm; /** * Message displayed in the top page. */ private String systemMessage; /** * Root directory of the system. */ public transient final File root; /** * All {@link Item}s keyed by their {@link Item#getName() name}s. */ /*package*/ transient final Map items = new CopyOnWriteMap.Tree(CaseInsensitiveComparator.INSTANCE); /** * The sole instance. */ private static Hudson theInstance; private transient volatile boolean isQuietingDown; private transient volatile boolean terminating; private List jdks = new ArrayList(); /** * Widgets on Hudson. */ private transient final List widgets = new CopyOnWriteArrayList(); private transient volatile DependencyGraph dependencyGraph; /** * Set of installed cluster nodes. * * We use this field with copy-on-write semantics. * This field has mutable list (to keep the serialization look clean), * but it shall never be modified. Only new completely populated slave * list can be set here. */ private volatile List slaves; /** * Quiet period. * * This is {@link Integer} so that we can initialize it to '5' for upgrading users. */ /*package*/ Integer quietPeriod; /** * {@link ListView}s. */ private List views; // can't initialize it eagerly for backward compatibility private transient MyView myView = new MyView(this); private transient final FingerprintMap fingerprintMap = new FingerprintMap(); /** * Loaded plugins. */ public transient final PluginManager pluginManager; public transient volatile TcpSlaveAgentListener tcpSlaveAgentListener; /** * List of registered {@link JobListener}s. */ private transient final CopyOnWriteList itemListeners = new CopyOnWriteList(); /** * List of registered {@link SCMListener}s. */ private transient final CopyOnWriteList scmListeners = new CopyOnWriteList(); /** * List of registered {@link ComputerListener}s. */ private transient final CopyOnWriteList computerListeners = new CopyOnWriteList(); /** * TCP slave agent port. * 0 for random, -1 to disable. */ private int slaveAgentPort =0; /** * All labels known to Hudson. This allows us to reuse the same label instances * as much as possible, even though that's not a strict requirement. */ private transient final ConcurrentHashMap labels = new ConcurrentHashMap(); private transient Set

* This value is useful for implementing some of the security features. */ public String getSecretKey() { return secretKey; } /** * Gets the SCM descriptor by name. Primarily used for making them web-visible. */ public Descriptor getScm(String shortClassName) { return findDescriptor(shortClassName,SCMS.SCMS); } /** * Gets the repository browser descriptor by name. Primarily used for making them web-visible. */ public Descriptor> getRepositoryBrowser(String shortClassName) { return findDescriptor(shortClassName,RepositoryBrowsers.LIST); } /** * Gets the builder descriptor by name. Primarily used for making them web-visible. */ public Descriptor getBuilder(String shortClassName) { return findDescriptor(shortClassName, BuildStep.BUILDERS); } /** * Gets the build wrapper descriptor by name. Primarily used for making them web-visible. */ public Descriptor getBuildWrapper(String shortClassName) { return findDescriptor(shortClassName, BuildWrappers.WRAPPERS); } /** * Gets the publisher descriptor by name. Primarily used for making them web-visible. */ public Descriptor getPublisher(String shortClassName) { return findDescriptor(shortClassName, BuildStep.PUBLISHERS); } /** * Gets the trigger descriptor by name. Primarily used for making them web-visible. */ public TriggerDescriptor getTrigger(String shortClassName) { return (TriggerDescriptor)findDescriptor(shortClassName, Triggers.TRIGGERS); } /** * Gets the {@link JobPropertyDescriptor} by name. Primarily used for making them web-visible. */ public JobPropertyDescriptor getJobProperty(String shortClassName) { // combining these two lines triggers javac bug. See issue #610. Descriptor d = findDescriptor(shortClassName, Jobs.PROPERTIES); return (JobPropertyDescriptor) d; } /** * Exposes {@link Descriptor} by its name to URL. * * After doing all the {@code getXXX(shortClassName)} methods, I finally realized that * this just doesn't scale. */ public Descriptor getDescriptor(String fullyQualifiedClassName) { for( Descriptor d : Descriptor.ALL ) if(d.clazz.getName().equals(fullyQualifiedClassName)) return d; return null; } /** * Gets the {@link SecurityRealm} descriptors by name. Primarily used for making them web-visible. */ public Descriptor getSecurityRealms(String shortClassName) { return findDescriptor(shortClassName,SecurityRealm.LIST); } /** * Finds a descriptor that has the specified name. */ private > Descriptor findDescriptor(String shortClassName, Collection> descriptors) { String name = '.'+shortClassName; for (Descriptor d : descriptors) { if(d.clazz.getName().endsWith(name)) return d; } return null; } /** * Adds a new {@link JobListener}. * * @deprecated * Use {@code getJobListners().add(l)} instead. */ public void addListener(JobListener l) { itemListeners.add(new JobListenerAdapter(l)); } /** * Deletes an existing {@link JobListener}. * * @deprecated * Use {@code getJobListners().remove(l)} instead. */ public boolean removeListener(JobListener l ) { return itemListeners.remove(new JobListenerAdapter(l)); } /** * Gets all the installed {@link ItemListener}s. */ public CopyOnWriteList getJobListeners() { return itemListeners; } /** * Gets all the installed {@link SCMListener}s. */ public CopyOnWriteList getSCMListeners() { return scmListeners; } /** * Gets all the installed {@link ComputerListener}s. */ public CopyOnWriteList getComputerListeners() { return computerListeners; } /** * Gets the plugin object from its short name. * *

* This allows URL hudson/plugin/ID to be served by the views * of the plugin class. */ public Plugin getPlugin(String shortName) { PluginWrapper p = pluginManager.getPlugin(shortName); if(p==null) return null; return p.getPlugin(); } /** * Gets the plugin object from its class. * *

* This allows easy storage of plugin information in the plugin singleton without * every plugin reimplementing the singleton pattern. * * @param clazz The plugin class (beware class-loader fun, this will probably only work * from within the hpi that defines the plugin class, it may or may not work in other cases) * * @return The plugin instance. */ @SuppressWarnings("unchecked") public

P getPlugin(Class

clazz) { PluginWrapper p = pluginManager.getPlugin(clazz); if(p==null) return null; return (P) p.getPlugin(); } /** * Gets the plugin objects from their super-class. * * @param clazz The plugin class (beware class-loader fun) * * @return The plugin instances. */ public

List

getPlugins(Class

clazz) { List

result = new ArrayList

(); for (PluginWrapper w: pluginManager.getPlugins(clazz)) { result.add((P)w.getPlugin()); } return Collections.unmodifiableList(result); } /** * Synonym to {@link #getNodeDescription()}. */ public String getSystemMessage() { return systemMessage; } public Launcher createLauncher(TaskListener listener) { return new LocalLauncher(listener); } /** * Updates {@link #computers} by using {@link #getSlaves()}. * *

* This method tries to reuse existing {@link Computer} objects * so that we won't upset {@link Executor}s running in it. */ private void updateComputerList() throws IOException { synchronized(computers) {// this synchronization is still necessary so that no two update happens concurrently Map byName = new HashMap(); for (Computer c : computers.values()) { if(c.getNode()==null) continue; // this computer is gone byName.put(c.getNode().getNodeName(),c); } Set old = new HashSet(computers.values()); Set used = new HashSet(); updateComputer(this, byName, used); for (Slave s : getSlaves()) updateComputer(s, byName, used); // find out what computers are removed, and kill off all executors. // when all executors exit, it will be removed from the computers map. // so don't remove too quickly old.removeAll(used); for (Computer c : old) { c.kill(); } } getQueue().scheduleMaintenance(); } private void updateComputer(Node n, Map byNameMap, Set used) { Computer c; c = byNameMap.get(n.getNodeName()); if (c!=null) { c.setNode(n); // reuse } else { if(n.getNumExecutors()>0) computers.put(n,c=n.createComputer()); } used.add(c); } /*package*/ void removeComputer(Computer computer) { for (Entry e : computers.entrySet()) { if (e.getValue() == computer) { computers.remove(e.getKey()); return; } } throw new IllegalStateException("Trying to remove unknown computer"); } public String getFullName() { return ""; } public String getFullDisplayName() { return ""; } /** * Returns the transient {@link Action}s associated with the top page. * *

* Adding {@link Action} is primarily useful for plugins to contribute * an item to the navigation bar of the top page. See existing {@link Action} * implementation for it affects the GUI. * *

* To register an {@link Action}, write code like * {@code Hudson.getInstance().getActions().add(...)} * * @return * Live list where the changes can be made. Can be empty but never null. * @since 1.172 */ public List getActions() { return actions; } /** * Gets just the immediate children of {@link Hudson}. * * @see #getAllItems(Class) */ public List getItems() { return new ArrayList(items.values()); } /** * Gets just the immediate children of {@link Hudson} but of the given type. */ public List getItems(Class type) { List r = new ArrayList(); for (TopLevelItem i : items.values()) if (type.isInstance(i)) r.add(type.cast(i)); return r; } /** * Gets all the {@link Item}s recursively in the {@link ItemGroup} tree * and filter them by the given type. */ public List getAllItems(Class type) { List r = new ArrayList(); Stack q = new Stack(); q.push(this); while(!q.isEmpty()) { ItemGroup parent = q.pop(); for (Item i : parent.getItems()) { if(type.isInstance(i)) r.add(type.cast(i)); if(i instanceof ItemGroup) q.push((ItemGroup)i); } } return r; } /** * Gets the list of all the projects. * *

* Since {@link Project} can only show up under {@link Hudson}, * no need to search recursively. */ public List getProjects() { return Util.createSubList(items.values(),Project.class); } /** * Gets the names of all the {@link Job}s. */ public Collection getJobNames() { List names = new ArrayList(); for (Job j : getAllItems(Job.class)) names.add(j.getFullName()); return names; } /** * Gets the names of all the {@link TopLevelItem}s. */ public Collection getTopLevelItemNames() { List names = new ArrayList(); for (TopLevelItem j : items.values()) names.add(j.getName()); return names; } /** * Every job belongs to us. * * @deprecated * why are you calling a method that always return true? */ @Deprecated public boolean contains(TopLevelItem view) { return true; } public synchronized View getView(String name) { if(views!=null) { for (ListView v : views) { if(v.getViewName().equals(name)) return v; } } if (this.getViewName().equals(name)) { return this; } else if (myView.getViewName().equals(name)) { return myView; } else return null; } /** * Gets the read-only list of all {@link View}s. */ @Exported public synchronized View[] getViews() { if(views==null) views = new ArrayList(); if(Functions.isAnonymous()) { View[] r = new View[views.size()+1]; views.toArray(r); // sort Views and put "all" at the very beginning r[r.length-1] = r[0]; Arrays.sort(r,1,r.length, View.SORTER); r[0] = this; return r; } else { // this is an authenticated user, so let's have the "my projects" view View[] r = new View[views.size()+2]; views.toArray(r); r[r.length-2] = r[0]; r[r.length-1] = r[1]; Arrays.sort(r,2,r.length, View.SORTER); r[0] = myView; r[1] = this; return r; } } public synchronized void deleteView(ListView view) throws IOException { if(views!=null) { views.remove(view); save(); } } public String getViewName() { return Messages.Hudson_ViewName(); } /** * Gets the read-only list of all {@link Computer}s. */ public Computer[] getComputers() { Computer[] r = computers.values().toArray(new Computer[computers.size()]); Arrays.sort(r,new Comparator() { public int compare(Computer lhs, Computer rhs) { if(lhs.getNode()==Hudson.this) return -1; if(rhs.getNode()==Hudson.this) return 1; return lhs.getDisplayName().compareTo(rhs.getDisplayName()); } }); return r; } /*package*/ Computer getComputer(Node n) { return computers.get(n); } public Computer getComputer(String name) { if(name.equals("(master)")) name = ""; for (Computer c : computers.values()) { if(c.getNode().getNodeName().equals(name)) return c; } return null; } /** * @deprecated * UI method. Not meant to be used programatically. */ public ComputerSet getComputer() { return new ComputerSet(); } public Computer toComputer() { return getComputer(this); } /** * Gets the label that exists on this system by the name. * * @return null if no such label exists. */ public Label getLabel(String name) { if(name==null) return null; while(true) { Label l = labels.get(name); if(l!=null) return l; // non-existent labels.putIfAbsent(name,new Label(name)); } } /** * Gets all the active labels in the current system. */ public Set

* Also note that when serving user requests from HTTP, you should always use * {@link HttpServletRequest} to determine the full URL, instead of using this * (this is because one host may have multiple names, and {@link HttpServletRequest} * accurately represents what the current user used.) * *

* This information is rather only meant to be useful for sending out messages * via non-HTTP channels, like SMTP or IRC, with a link back to Hudson website. * * @return * This method returns null if this parameter is not configured by the user. * The caller must gracefully deal with this situation. * The returned URL will always have the trailing '/'. * @since 1.66 * @see Descriptor#getCheckUrl(String) */ public String getRootUrl() { // for compatibility. the actual data is stored in Mailer String url = Mailer.DESCRIPTOR.getUrl(); if(url!=null) return url; StaplerRequest req = Stapler.getCurrentRequest(); if(req!=null) { StringBuilder buf = new StringBuilder(); buf.append("http://"); buf.append(req.getServerName()); if(req.getServerPort()!=80) buf.append(':').append(req.getServerPort()); buf.append(req.getContextPath()).append('/'); return buf.toString(); } return null; } public File getRootDir() { return root; } public FilePath getWorkspaceFor(TopLevelItem item) { return new FilePath(new File(item.getRootDir(),"workspace")); } public FilePath getRootPath() { return new FilePath(getRootDir()); } public FilePath createPath(String absolutePath) { return new FilePath((VirtualChannel)null,absolutePath); } public ClockDifference getClockDifference() { return ClockDifference.ZERO; } /** * A convenience method to check if there's some security * restrictions in place. */ public boolean isUseSecurity() { return securityRealm!=SecurityRealm.NO_AUTHENTICATION || authorizationStrategy!=AuthorizationStrategy.UNSECURED; } /** * Returns the constant that captures the three basic security modes * in Hudson. */ public SecurityMode getSecurity() { // fix the variable so that this code works under concurrent modification to securityRealm. SecurityRealm realm = securityRealm; if(realm==SecurityRealm.NO_AUTHENTICATION) return SecurityMode.UNSECURED; if(realm instanceof LegacySecurityRealm) return SecurityMode.LEGACY; return SecurityMode.SECURED; } /** * @return * never null. */ public SecurityRealm getSecurityRealm() { return securityRealm; } public void setSecurityRealm(SecurityRealm securityRealm) { if(securityRealm==null) securityRealm= SecurityRealm.NO_AUTHENTICATION; this.securityRealm = securityRealm; SecurityComponents sc = securityRealm.createSecurityComponents(); HudsonFilter.AUTHENTICATION_MANAGER.setDelegate(sc.manager); HudsonFilter.USER_DETAILS_SERVICE_PROXY.setDelegate(sc.userDetails); } /** * Returns the root {@link ACL}. * * @see AuthorizationStrategy#getRootACL() */ public ACL getACL() { return authorizationStrategy.getRootACL(); } /** * @return * never null. */ public AuthorizationStrategy getAuthorizationStrategy() { return authorizationStrategy; } /** * Returns true if Hudson is quieting down. *

* No further jobs will be executed unless it * can be finished while other current pending builds * are still in progress. */ public boolean isQuietingDown() { return isQuietingDown; } /** * Returns true if the container initiated the termination of the web application. */ public boolean isTerminating() { return terminating; } public void setNumExecutors(int n) throws IOException { this.numExecutors = n; save(); } /** * @deprecated * Left only for the compatibility of URLs. * Should not be invoked for any other purpose. */ public TopLevelItem getJob(String name) { return getItem(name); } /** * @deprecated * Used only for mapping jobs to URL in a case-insensitive fashion. */ public TopLevelItem getJobCaseInsensitive(String name) { for (Entry e : items.entrySet()) { if(Functions.toEmailSafeString(e.getKey()).equalsIgnoreCase(Functions.toEmailSafeString(name))) return e.getValue(); } return null; } /** * {@inheritDoc}. * * Note that the look up is case-insensitive. */ @Override public TopLevelItem getItem(String name) { return items.get(name); } public File getRootDirFor(TopLevelItem child) { return getRootDirFor(child.getName()); } private File getRootDirFor(String name) { return new File(new File(getRootDir(),"jobs"), name); } /** * Gets the {@link Item} object by its full name. * Full names are like path names, where each name of {@link Item} is * combined by '/'. * * @return * null if either such {@link Item} doesn't exist under the given full name, * or it exists but it's no an instance of the given type. */ public T getItemByFullName(String fullName, Class type) { StringTokenizer tokens = new StringTokenizer(fullName,"/"); ItemGroup parent = this; while(true) { Item item = parent.getItem(tokens.nextToken()); if(!tokens.hasMoreTokens()) { if(type.isInstance(item)) return type.cast(item); else return null; } if(!(item instanceof ItemGroup)) return null; // this item can't have any children parent = (ItemGroup) item; } } public Item getItemByFullName(String fullName) { return getItemByFullName(fullName,Item.class); } /** * Gets the user of the given name. * * @return * This method returns a non-null object for any user name, without validation. */ public User getUser(String name) { return User.get(name); } /** * Creates a new job. * * @throws IllegalArgumentException * if the project of the given name already exists. */ public synchronized TopLevelItem createProject( TopLevelItemDescriptor type, String name ) throws IOException { if(items.containsKey(name)) throw new IllegalArgumentException(); TopLevelItem item; try { item = type.newInstance(name); } catch (Exception e) { throw new IllegalArgumentException(e); } item.save(); items.put(name,item); return item; } /** * Called in response to {@link Job#doDoDelete(StaplerRequest, StaplerResponse)} */ /*package*/ void deleteJob(TopLevelItem item) throws IOException { for (ItemListener l : itemListeners) l.onDeleted(item); items.remove(item.getName()); if(views!=null) { for (ListView v : views) { synchronized(v) { v.jobNames.remove(item.getName()); } } save(); } } /** * Called by {@link Job#renameTo(String)} to update relevant data structure. * assumed to be synchronized on Hudson by the caller. */ /*package*/ void onRenamed(TopLevelItem job, String oldName, String newName) throws IOException { items.remove(oldName); items.put(newName,job); if(views!=null) { for (ListView v : views) { synchronized(v) { if(v.jobNames.remove(oldName)) v.jobNames.add(newName); } } save(); } } public FingerprintMap getFingerprintMap() { return fingerprintMap; } // if no finger print matches, display "not found page". public Object getFingerprint( String md5sum ) throws IOException { Fingerprint r = fingerprintMap.get(md5sum); if(r==null) return new NoFingerprintMatch(md5sum); else return r; } /** * Gets a {@link Fingerprint} object if it exists. * Otherwise null. */ public Fingerprint _getFingerprint( String md5sum ) throws IOException { return fingerprintMap.get(md5sum); } /** * The file we save our configuration. */ private XmlFile getConfigFile() { return new XmlFile(XSTREAM, new File(root,"config.xml")); } public int getNumExecutors() { return numExecutors; } public Mode getMode() { return mode; } public Set

* See https://bugzilla.mozilla.org/show_bug.cgi?id=89419 */ public void doNocacheImages( StaplerRequest req, StaplerResponse rsp ) throws IOException { String path = req.getRestOfPath(); if(path.length()==0) path = "/"; if(path.indexOf("..")!=-1 || path.length()<1) { // don't serve anything other than files in the artifacts dir rsp.sendError(SC_BAD_REQUEST); return; } File f = new File(req.getServletContext().getRealPath("/images"),path.substring(1)); if(!f.exists()) { rsp.sendError(SC_NOT_FOUND); return; } if(f.isDirectory()) { // listing not allowed rsp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } FileInputStream in = new FileInputStream(f); // serve the file String contentType = req.getServletContext().getMimeType(f.getPath()); rsp.setContentType(contentType); rsp.setContentLength((int)f.length()); Util.copyStream(in,rsp.getOutputStream()); in.close(); } /** * For debugging. Expose URL to perform GC. */ public void doGc(StaplerResponse rsp) throws IOException { System.gc(); rsp.setStatus(HttpServletResponse.SC_OK); rsp.setContentType("text/plain"); rsp.getWriter().println("GCed"); } /** * Perform a restart of Hudson, if we can. */ public void doRestart(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { requirePOST(); checkPermission(ADMINISTER); try { Lifecycle.get().restart(); servletContext.setAttribute("app",new HudsonIsRestarting()); rsp.sendRedirect2("."); } catch (UnsupportedOperationException e) { sendError("Restart is not supported in this running mode.",req,rsp); } catch (IOException e) { sendError(e,req,rsp); } catch (InterruptedException e) { sendError(e,req,rsp); } } /** * Shutdown the system. * @since 1.161 */ public void doExit( StaplerRequest req, StaplerResponse rsp ) throws IOException { checkPermission(ADMINISTER); LOGGER.severe(String.format("Shutting down VM as requested by %s from %s", getAuthentication(), req.getRemoteAddr())); rsp.setStatus(HttpServletResponse.SC_OK); rsp.setContentType("text/plain"); PrintWriter w = rsp.getWriter(); w.println("Shutting down"); w.close(); System.exit(0); } /** * Gets the {@link Authentication} object that represents the user * associated with the current request. */ public static Authentication getAuthentication() { Authentication a = SecurityContextHolder.getContext().getAuthentication(); // on Tomcat while serving the login page, this is null despite the fact // that we have filters. Looking at the stack trace, Tomcat doesn't seem to // run the request through filters when this is the login request. // see http://www.nabble.com/Matrix-authorization-problem-tp14602081p14886312.html if(a==null) a = new AnonymousAuthenticationToken("anonymous","anonymous",new GrantedAuthority[]{new GrantedAuthorityImpl("anonymous")}); return a; } /** * Configure the logging level. */ public void doConfigLogger(StaplerResponse rsp, @QueryParameter String name, @QueryParameter String level) throws IOException { checkPermission(ADMINISTER); Level lv; if(level.equals("inherit")) lv = null; else lv = Level.parse(level.toUpperCase()); Logger.getLogger(name).setLevel(lv); rsp.sendRedirect2("log"); } /** * For system diagnostics. * Run arbitrary Groovy script. */ public void doScript( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { // ability to run arbitrary script is dangerous checkPermission(ADMINISTER); String text = req.getParameter("script"); if(text!=null) { try { req.setAttribute("output", RemotingDiagnostics.executeGroovy(text, MasterComputer.localChannel)); } catch (InterruptedException e) { throw new ServletException(e); } } req.getView(this,"_script.jelly").forward(req,rsp); } /** * Sign up for the user account. */ public void doSignup( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { req.getView(getSecurityRealm(),"signup.jelly").forward(req,rsp); } /** * Changes the icon size by changing the cookie */ public void doIconSize( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { String qs = req.getQueryString(); if(qs==null || !ICON_SIZE.matcher(qs).matches()) throw new ServletException(); Cookie cookie = new Cookie("iconSize", qs); cookie.setMaxAge(/* ~4 mo. */9999999); // #762 rsp.addCookie(cookie); String ref = req.getHeader("Referer"); if(ref==null) ref="."; rsp.sendRedirect2(ref); } public void doFingerprintCleanup(StaplerResponse rsp) throws IOException { FingerprintCleanupThread.invoke(); rsp.setStatus(HttpServletResponse.SC_OK); rsp.setContentType("text/plain"); rsp.getWriter().println("Invoked"); } public void doWorkspaceCleanup(StaplerResponse rsp) throws IOException { WorkspaceCleanupThread.invoke(); rsp.setStatus(HttpServletResponse.SC_OK); rsp.setContentType("text/plain"); rsp.getWriter().println("Invoked"); } /** * Checks if the path is a valid path. */ public void doCheckLocalFSRoot( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { // this can be used to check the existence of a file on the server, so needs to be protected new FormFieldValidator.WorkspaceDirectory(req,rsp,true).process(); } /** * Checks if the JAVA_HOME is a valid JAVA_HOME path. */ public void doJavaHomeCheck( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { // this can be used to check the existence of a file on the server, so needs to be protected new FormFieldValidator(req,rsp,true) { public void check() throws IOException, ServletException { File f = getFileParameter("value"); if(!f.isDirectory()) { error(Messages.Hudson_NotADirectory(f)); return; } File toolsJar = new File(f,"lib/tools.jar"); File mac = new File(f,"lib/dt.jar"); if(!toolsJar.exists() && !mac.exists()) { error(Messages.Hudson_NotJDKDir(f)); return; } ok(); } }.process(); } /** * If the user chose the default JDK, make sure we got 'java' in PATH. */ public void doDefaultJDKCheck( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { new FormFieldValidator(req,rsp,true) { public void check() throws IOException, ServletException { String v = request.getParameter("value"); if(!v.equals("(Default)")) // assume the user configured named ones properly in system config --- // or else system config should have reported form field validation errors. ok(); else { // default JDK selected. Does such java really exist? if(JDK.isDefaultJDKValid(Hudson.this)) ok(); else errorWithMarkup(Messages.Hudson_NoJavaInPath(request.getContextPath())); } } }.process(); } /** * Checks if the top-level item with the given name exists. */ public void doItemExistsCheck(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { // this method can be used to check if a file exists anywhere in the file system, // so it should be protected. new FormFieldValidator(req,rsp,true) { protected void check() throws IOException, ServletException { String job = fixEmpty(request.getParameter("value")); if(job==null) { ok(); // nothing is entered yet return; } if(getItem(job)==null) ok(); else error(Messages.Hudson_JobAlreadyExists(job)); } }.process(); } /** * Checks if the value for a field is set; if not an error or warning text is displayed. * If the parameter "value" is not set then the parameter "errorText" is displayed * as an error text. If the parameter "errorText" is not set, then the parameter "warningText" is * displayed as a warning text. *

* If the text is set and the parameter "type" is set, it will validate that the value is of the * correct type. Supported types are "number, "number-positive" and "number-negative". * @param req containing the parameter value and the errorText to display if the value isnt set * @param rsp used by FormFieldValidator * @throws IOException thrown by FormFieldValidator.check() * @throws ServletException thrown by FormFieldValidator.check() */ public void doFieldCheck(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { new FormFieldValidator(req, rsp, false) { /** * Display the error text or warning text. */ private void fieldCheckFailed() throws IOException, ServletException { String v = fixEmpty(request.getParameter("errorText")); if (v != null) { error(v); return; } v = fixEmpty(request.getParameter("warningText")); if (v != null) { warning(v); return; } error("No error or warning text was set for fieldCheck()."); return; } /** * Checks if the value is of the correct type. * @param type the type of string * @param value the actual value to check * @return true, if the type was valid; false otherwise */ private boolean checkType(String type, String value) throws IOException, ServletException { try { if (type.equalsIgnoreCase("number")) { NumberFormat.getInstance().parse(value); } else if (type.equalsIgnoreCase("number-positive")) { if (NumberFormat.getInstance().parse(value).floatValue() <= 0) { error(Messages.Hudson_NotAPositiveNumber()); return false; } } else if (type.equalsIgnoreCase("number-negative")) { if (NumberFormat.getInstance().parse(value).floatValue() >= 0) { error(Messages.Hudson_NotANegativeNumber()); return false; } } } catch (ParseException e) { error(Messages.Hudson_NotANumber()); return false; } return true; } @Override protected void check() throws IOException, ServletException { String value = fixEmpty(request.getParameter("value")); if (value == null) { fieldCheckFailed(); return; } String type = fixEmpty(request.getParameter("type")); if (type != null) { if (!checkType(type, value)) { return; } } ok(); } }.process(); } /** * Performs syntactical check on the remote FS for slaves. * * TODO: find a better home for this method. */ public void doRemoteFSCheck(StaplerRequest req, StaplerResponse rsp, @QueryParameter final String value) throws IOException, ServletException { new FormFieldValidator(req,rsp,false) { protected void check() throws IOException, ServletException { if(Util.fixEmptyAndTrim(value)==null) { error("Remote directory is mandatory"); return; } if(value.startsWith("\\\\") || value.startsWith("/net/")) { warning("Are you sure you want to use network mounted file system for FS root? " + "Note that this directory needs not be visible to the master."); return; } ok(); } }.process(); } /** * Serves static resources placed along with Jelly view files. *

* This method can serve a lot of files, so care needs to be taken * to make this method secure. It's not clear to me what's the best * strategy here, though the current implementation is based on * file extensions. */ public void doResources(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { String path = req.getRestOfPath(); // cut off the "..." portion of /resources/.../path/to/file // as this is only used to make path unique (which in turn // allows us to set a long expiration date path = path.substring(1); path = path.substring(path.indexOf('/')+1); int idx = path.lastIndexOf('.'); String extension = path.substring(idx+1); if(ALLOWED_RESOURCE_EXTENSIONS.contains(extension)) { URL url = pluginManager.uberClassLoader.getResource(path); if(url!=null) { long expires = MetaClass.NO_CACHE ? 0 : 365L * 24 * 60 * 60 * 1000; /*1 year*/ rsp.serveFile(req,url,expires); return; } } rsp.sendError(HttpServletResponse.SC_NOT_FOUND); } /** * Extension list that {@link #doResources(StaplerRequest, StaplerResponse)} can serve. * This set is mutable to allow plugins to add additional extensions. */ public static final Set ALLOWED_RESOURCE_EXTENSIONS = new HashSet(Arrays.asList( "js|css|jpeg|jpg|png|gif|html|htm".split("\\|") )); /** * Checks if container uses UTF-8 to decode URLs. See * http://hudson.gotdns.com/wiki/display/HUDSON/Tomcat#Tomcat-i18n * * @param req containing the parameter value * @param rsp used by FormFieldValidator * @throws IOException thrown by FormFieldValidator.check() * @throws ServletException thrown by FormFieldValidator.check() */ public void doCheckURIEncoding(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { new FormFieldValidator(req, rsp, true) { @Override protected void check() throws IOException, ServletException { request.setCharacterEncoding("UTF-8"); // expected is non-ASCII String final String expected = "\u57f7\u4e8b"; final String value = fixEmpty(request.getParameter("value")); if (!expected.equals(value)) { warningWithMarkup(Messages.Hudson_NotUsesUTF8ToDecodeURL()); return; } ok(); } }.process(); } /** * Does not check when system default encoding is "ISO-8859-1". */ public static boolean isCheckURIEncodingEnabled() { return !"ISO-8859-1".equalsIgnoreCase(System.getProperty("file.encoding")); } public static boolean isWindows() { return File.pathSeparatorChar==';'; } /** * Returns all {@code CVSROOT} strings used in the current Hudson installation. * *

* Ideally this shouldn't be defined in here * but EL doesn't provide a convenient way of invoking a static function, * so I'm putting it here for now. */ public Set getAllCvsRoots() { Set r = new TreeSet(); for( AbstractProject p : getAllItems(AbstractProject.class) ) { SCM scm = p.getScm(); if (scm instanceof CVSSCM) { CVSSCM cvsscm = (CVSSCM) scm; r.add(cvsscm.getCvsRoot()); } } return r; } /** * Rebuilds the dependency map. */ public void rebuildDependencyGraph() { dependencyGraph = new DependencyGraph(); } public DependencyGraph getDependencyGraph() { return dependencyGraph; } // for Jelly public List getManagementLinks() { return ManagementLink.LIST; } /** * Gets the {@link Widget}s registered on this object. * *

* Plugins who wish to contribute boxes on the side panel can add widgets * by {@code getWidgets().add(new MyWidget())} from {@link Plugin#start()}. */ public List getWidgets() { return widgets; } public Object getTarget() { try { checkPermission(READ); } catch (AccessDeniedException e) { String rest = Stapler.getCurrentRequest().getRestOfPath(); if(rest.startsWith("/login") || rest.startsWith("/logout") || rest.startsWith("/accessDenied") || rest.startsWith("/signup") || rest.startsWith("/jnlpJars/") || rest.startsWith("/securityRealm")) return this; // URLs that are always visible without READ permission throw e; } return this; } public static final class MasterComputer extends Computer { private MasterComputer() { super(Hudson.getInstance()); } /** * Returns "" to match with {@link Hudson#getNodeName()}. */ public String getName() { return ""; } @Override public String getDisplayName() { return Messages.Hudson_Computer_DisplayName(); } @Override public String getCaption() { return Messages.Hudson_Computer_Caption(); } public String getUrl() { return "computer/(master)/"; } public RetentionStrategy getRetentionStrategy() { return RetentionStrategy.NOOP; } @Override public VirtualChannel getChannel() { return localChannel; } @Override public Charset getDefaultCharset() { return Charset.defaultCharset(); } public List getLogRecords() throws IOException, InterruptedException { return logRecords; } public void doLaunchSlaveAgent(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { // this computer never returns null from channel, so // this method shall never be invoked. rsp.sendError(SC_NOT_FOUND); } public void launch() { // noop } /** * {@link LocalChannel} instance that can be used to execute programs locally. */ public static final LocalChannel localChannel = new LocalChannel(threadPoolForRemoting); } /** * @deprecated * Use {@link #checkPermission(Permission)} */ public static boolean adminCheck() throws IOException { return adminCheck(Stapler.getCurrentRequest(), Stapler.getCurrentResponse()); } /** * @deprecated * Use {@link #checkPermission(Permission)} */ public static boolean adminCheck(StaplerRequest req,StaplerResponse rsp) throws IOException { if (isAdmin(req)) return true; rsp.sendError(StaplerResponse.SC_FORBIDDEN); return false; } /** * Checks if the current user (for which we are processing the current request) * has the admin access. * * @deprecated * This method is deprecated when Hudson moved from simple Unix root-like model * of "admin gets to do everything, and others don't have any privilege" to more * complex {@link ACL} and {@link Permission} based scheme. * *

* For a quick migration, use {@code Hudson.getInstance().getACL().hasPermission(Hudson.ADMINISTER)} * To check if the user has the 'administer' role in Hudson. * *

* But ideally, your plugin should first identify a suitable {@link Permission} (or create one, * if appropriate), then identify a suitable {@link AccessControlled} object to check its permission * against. */ public static boolean isAdmin() { return Hudson.getInstance().getACL().hasPermission(ADMINISTER); } /** * @deprecated * Define a custom {@link Permission} and check against ACL. * See {@link #isAdmin()} for more instructions. */ public static boolean isAdmin(StaplerRequest req) { return isAdmin(); } /** * Live view of recent {@link LogRecord}s produced by Hudson. */ public static List logRecords = Collections.emptyList(); // initialized to dummy value to avoid NPE /** * Thread-safe reusable {@link XStream}. */ public static final XStream XSTREAM = new XStream2(); private static final int TWICE_CPU_NUM = Runtime.getRuntime().availableProcessors() * 2; /** * Thread pool used to load configuration in parallel, to improve the start up time. *

* The idea here is to overlap the CPU and I/O, so we want more threads than CPU numbers. */ /*package*/ static final ExecutorService threadPoolForLoad = new ThreadPoolExecutor( TWICE_CPU_NUM, TWICE_CPU_NUM, 5L, TimeUnit.SECONDS, new LinkedBlockingQueue(), new DaemonThreadFactory()); /** * Version number of this Hudson. */ public static String VERSION="?"; /** * Prefix to static resources like images and javascripts in the war file. * Either "" or strings like "/static/VERSION", which avoids Hudson to pick up * stale cache when the user upgrades to a different version. *

* Value computed in {@link WebAppMain}. */ public static String RESOURCE_PATH = ""; /** * Prefix to resources alongside view scripts. * Strings like "/resources/VERSION", which avoids Hudson to pick up * stale cache when the user upgrades to a different version. *

* Value computed in {@link WebAppMain}. */ public static String VIEW_RESOURCE_PATH = "/resources/TBD"; public static boolean PARALLEL_LOAD = Boolean.getBoolean(Hudson.class.getName()+".parallelLoad"); public static boolean KILL_AFTER_LOAD = Boolean.getBoolean(Hudson.class.getName()+".killAfterLoad"); public static boolean LOG_STARTUP_PERFORMANCE = Boolean.getBoolean(Hudson.class.getName()+".logStartupPerformance"); private static final Logger LOGGER = Logger.getLogger(Hudson.class.getName()); private static final Pattern ICON_SIZE = Pattern.compile("\\d+x\\d+"); public static final PermissionGroup PERMISSIONS = new PermissionGroup(Hudson.class,Messages._Hudson_Permissions_Title()); public static final Permission ADMINISTER = new Permission(PERMISSIONS,"Administer",Messages._Hudson_AdministerPermission_Description(), Permission.FULL_CONTROL); public static final Permission READ = new Permission(PERMISSIONS,"Read",Messages._Hudson_ReadPermission_Description(),Permission.READ); static { XSTREAM.alias("hudson",Hudson.class); XSTREAM.alias("slave",Slave.class); XSTREAM.alias("jdk",JDK.class); // for backward compatibility with <1.75, recognize the tag name "view" as well. XSTREAM.alias("view", ListView.class); XSTREAM.alias("listView", ListView.class); // this seems to be necessary to force registration of converter early enough Mode.class.getEnumConstants(); } }