mirror of https://github.com/jenkinsci/jenkins.git
Merge in changes that implement the cross-site request forgery crumb feature.
git-svn-id: https://hudson.dev.java.net/svn/hudson/trunk/hudson/main@18738 71c3de6d-444a-0410-be80-ed276b4c234a
This commit is contained in:
parent
fa48bd2252
commit
8c4e97d171
|
|
@ -1,11 +1,17 @@
|
|||
package hudson.cli;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Reader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Creates a capacity-unlimited bi-directional {@link InputStream}/{@link OutputStream} pair over
|
||||
|
|
@ -34,12 +40,17 @@ public class FullDuplexHttpStream {
|
|||
public FullDuplexHttpStream(URL target) throws IOException {
|
||||
this.target = target;
|
||||
|
||||
CrumbData crumbData = new CrumbData();
|
||||
|
||||
// server->client
|
||||
HttpURLConnection con = (HttpURLConnection) target.openConnection();
|
||||
con.setDoOutput(true); // request POST to avoid caching
|
||||
con.setRequestMethod("POST");
|
||||
con.addRequestProperty("Session",uuid.toString());
|
||||
con.addRequestProperty("Side","download");
|
||||
if(crumbData.isValid) {
|
||||
con.addRequestProperty(crumbData.crumbName, crumbData.crumb);
|
||||
}
|
||||
con.getOutputStream().close();
|
||||
input = con.getInputStream();
|
||||
// make sure we hit the right URL
|
||||
|
|
@ -53,8 +64,54 @@ public class FullDuplexHttpStream {
|
|||
con.setChunkedStreamingMode(0);
|
||||
con.addRequestProperty("Session",uuid.toString());
|
||||
con.addRequestProperty("Side","upload");
|
||||
if(crumbData.isValid) {
|
||||
con.addRequestProperty(crumbData.crumbName, crumbData.crumb);
|
||||
}
|
||||
output = con.getOutputStream();
|
||||
}
|
||||
|
||||
static final int BLOCK_SIZE = 1024;
|
||||
static final Logger LOGGER = Logger.getLogger(FullDuplexHttpStream.class.getName());
|
||||
|
||||
private final class CrumbData {
|
||||
String crumbName;
|
||||
String crumb;
|
||||
boolean isValid;
|
||||
|
||||
private CrumbData() {
|
||||
this.crumbName = "";
|
||||
this.crumb = "";
|
||||
this.isValid = false;
|
||||
getData();
|
||||
}
|
||||
|
||||
private void getData() {
|
||||
try {
|
||||
String base = createCrumbUrlBase();
|
||||
crumbName = readData(base+"?xpath=/*/crumbRequestField/text()");
|
||||
crumb = readData(base+"?xpath=/*/crumb/text()");
|
||||
isValid = true;
|
||||
LOGGER.fine("Crumb data: "+crumbName+"="+crumb);
|
||||
}
|
||||
catch (IOException e) {
|
||||
LOGGER.log(Level.WARNING,"Failed to get crumb data",e);
|
||||
}
|
||||
}
|
||||
|
||||
private String createCrumbUrlBase() {
|
||||
String url = target.toExternalForm();
|
||||
return new StringBuilder(url.substring(0, url.lastIndexOf("/cli"))).append("/crumbIssuer/api/xml/").toString();
|
||||
}
|
||||
|
||||
private String readData(String dest) throws IOException {
|
||||
HttpURLConnection con = (HttpURLConnection) new URL(dest).openConnection();
|
||||
try {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream()));
|
||||
return reader.readLine();
|
||||
}
|
||||
finally {
|
||||
con.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11
core/pom.xml
11
core/pom.xml
|
|
@ -702,6 +702,17 @@ THE SOFTWARE.
|
|||
<artifactId>jinterop-wmi</artifactId>
|
||||
<version>1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jvnet.hudson</groupId>
|
||||
<artifactId>htmlunit</artifactId>
|
||||
<version>2.2-hudson-9</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>xalan</groupId>
|
||||
<artifactId>xalan</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- offline profiler API to put in the classpath if we need it -->
|
||||
<!--dependency>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import hudson.security.AccessControlled;
|
|||
import hudson.security.AuthorizationStrategy;
|
||||
import hudson.security.Permission;
|
||||
import hudson.security.SecurityRealm;
|
||||
import hudson.security.csrf.CrumbIssuer;
|
||||
import hudson.slaves.Cloud;
|
||||
import hudson.slaves.ComputerLauncher;
|
||||
import hudson.slaves.NodeProperty;
|
||||
|
|
@ -1132,6 +1133,56 @@ public class Functions {
|
|||
return body;
|
||||
}
|
||||
|
||||
public static List<Descriptor<CrumbIssuer>> getCrumbIssuerDescriptors() {
|
||||
return CrumbIssuer.all();
|
||||
}
|
||||
|
||||
public static String getCrumb(StaplerRequest req) {
|
||||
CrumbIssuer issuer = Hudson.getInstance().getCrumbIssuer();
|
||||
if (issuer != null) {
|
||||
return issuer.getCrumb(req);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public static String getCrumbRequestField() {
|
||||
CrumbIssuer issuer = Hudson.getInstance().getCrumbIssuer();
|
||||
if (issuer != null) {
|
||||
return issuer.getDescriptor().getCrumbRequestField();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public static String getCrumbAsJSONParameterBlock(StaplerRequest req) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (Hudson.getInstance().isUseCrumbs()) {
|
||||
builder.append("parameters:{\"");
|
||||
builder.append(getCrumbRequestField());
|
||||
builder.append("\":\"");
|
||||
builder.append(getCrumb(req));
|
||||
builder.append("\"}");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static String getReplaceDescriptionInvoker(StaplerRequest req) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (isAutoRefresh(req)) {
|
||||
builder.append("null");
|
||||
} else {
|
||||
builder.append("'return replaceDescription(");
|
||||
builder.append("\\'");
|
||||
builder.append(getCrumbRequestField());
|
||||
builder.append("\\',\\'");
|
||||
builder.append(getCrumb(req));
|
||||
builder.append("\\'");
|
||||
builder.append(");'");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static final Pattern SCHEME = Pattern.compile("[a-z]+://.+");
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package hudson.model;
|
|||
import hudson.Extension;
|
||||
import hudson.ExtensionList;
|
||||
import hudson.ExtensionPoint;
|
||||
import hudson.security.csrf.CrumbIssuer;
|
||||
import hudson.util.QuotedStringTokenizer;
|
||||
import hudson.util.TextFile;
|
||||
import hudson.util.TimeUnit2;
|
||||
|
|
@ -38,9 +39,22 @@ public class DownloadService extends PageDecorator {
|
|||
StringBuilder buf = new StringBuilder();
|
||||
if(Hudson.getInstance().hasPermission(Hudson.READ)) {
|
||||
long now = System.currentTimeMillis();
|
||||
CrumbIssuer ci = Hudson.getInstance().getCrumbIssuer();
|
||||
String crumbName = null;
|
||||
String crumb = null;
|
||||
if(ci!=null) {
|
||||
crumbName = ci.getCrumbRequestField();
|
||||
crumb = ci.getCrumb();
|
||||
}
|
||||
for (Downloadable d : Downloadable.all()) {
|
||||
if(d.getDue()<now) {
|
||||
buf.append("<script>downloadService.download(")
|
||||
buf.append("<script>");
|
||||
if(ci!=null) {
|
||||
buf.append("downloadService.crumbName="+QuotedStringTokenizer.quote(crumbName))
|
||||
.append(";downloadService.crumb="+QuotedStringTokenizer.quote(crumb))
|
||||
.append(';');
|
||||
}
|
||||
buf.append("downloadService.download(")
|
||||
.append(QuotedStringTokenizer.quote(d.getId()))
|
||||
.append(',')
|
||||
.append(QuotedStringTokenizer.quote(d.getUrl()))
|
||||
|
|
@ -197,3 +211,4 @@ public class DownloadService extends PageDecorator {
|
|||
private static final Logger LOGGER = Logger.getLogger(Downloadable.class.getName());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt, Koichi Fujikawa, Red Hat, Inc., Seiji Sogabe, Stephen Connolly, Tom Huybrechts
|
||||
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt, Koichi Fujikawa, Red Hat, Inc., Seiji Sogabe, Stephen Connolly, Tom Huybrechts, Yahoo! 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
|
||||
|
|
@ -79,6 +79,9 @@ import hudson.security.Permission;
|
|||
import hudson.security.PermissionGroup;
|
||||
import hudson.security.SecurityMode;
|
||||
import hudson.security.SecurityRealm;
|
||||
import hudson.security.csrf.CrumbFilter;
|
||||
import hudson.security.csrf.CrumbIssuer;
|
||||
import hudson.security.csrf.CrumbIssuerDescriptor;
|
||||
import hudson.slaves.ComputerListener;
|
||||
import hudson.slaves.NodeProperty;
|
||||
import hudson.slaves.NodePropertyDescriptor;
|
||||
|
|
@ -416,6 +419,13 @@ public final class Hudson extends Node implements ItemGroup<TopLevelItem>, Stapl
|
|||
*/
|
||||
private String label="";
|
||||
|
||||
private Boolean useCrumbs;
|
||||
|
||||
/**
|
||||
* {@link hudson.security.csrf.CrumbIssuer}
|
||||
*/
|
||||
private volatile CrumbIssuer crumbIssuer;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
|
@ -1582,6 +1592,10 @@ public final class Hudson extends Node implements ItemGroup<TopLevelItem>, Stapl
|
|||
return securityRealm!=SecurityRealm.NO_AUTHENTICATION || authorizationStrategy!=AuthorizationStrategy.UNSECURED;
|
||||
}
|
||||
|
||||
public boolean isUseCrumbs() {
|
||||
return (useCrumbs != null) && useCrumbs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the constant that captures the three basic security modes
|
||||
* in Hudson.
|
||||
|
|
@ -2047,6 +2061,9 @@ public final class Hudson extends Node implements ItemGroup<TopLevelItem>, Stapl
|
|||
setSecurityRealm(SecurityRealm.NO_AUTHENTICATION);
|
||||
}
|
||||
|
||||
// Initialize the filter with the crumb issuer
|
||||
setCrumbIssuer(crumbIssuer);
|
||||
|
||||
LOGGER.info(String.format("Took %s ms to load",System.currentTimeMillis()-startTime));
|
||||
if(KILL_AFTER_LOAD)
|
||||
System.exit(0);
|
||||
|
|
@ -2149,6 +2166,15 @@ public final class Hudson extends Node implements ItemGroup<TopLevelItem>, Stapl
|
|||
authorizationStrategy = AuthorizationStrategy.UNSECURED;
|
||||
}
|
||||
|
||||
if (json.has("csrf")) {
|
||||
useCrumbs = true;
|
||||
JSONObject csrf = json.getJSONObject("csrf");
|
||||
setCrumbIssuer(CrumbIssuer.all().newInstanceFromRadioList(csrf, "issuer"));
|
||||
} else {
|
||||
useCrumbs = null;
|
||||
setCrumbIssuer(null);
|
||||
}
|
||||
|
||||
noUsageStatistics = json.has("usageStatisticsCollected") ? null : true;
|
||||
|
||||
{
|
||||
|
|
@ -2219,6 +2245,9 @@ public final class Hudson extends Node implements ItemGroup<TopLevelItem>, Stapl
|
|||
for( PageDecorator d : PageDecorator.all() )
|
||||
result &= configureDescriptor(req,json,d);
|
||||
|
||||
for( Descriptor<CrumbIssuer> d : CrumbIssuer.all() )
|
||||
result &= configureDescriptor(req,json, d);
|
||||
|
||||
for( ToolDescriptor d : ToolInstallation.all() )
|
||||
result &= configureDescriptor(req,json,d);
|
||||
|
||||
|
|
@ -2247,6 +2276,19 @@ public final class Hudson extends Node implements ItemGroup<TopLevelItem>, Stapl
|
|||
}
|
||||
}
|
||||
|
||||
public CrumbIssuer getCrumbIssuer() {
|
||||
return crumbIssuer;
|
||||
}
|
||||
|
||||
public void setCrumbIssuer(CrumbIssuer issuer) {
|
||||
crumbIssuer = issuer;
|
||||
CrumbFilter.get(servletContext).setCrumbIssuer(issuer);
|
||||
}
|
||||
|
||||
public void setUseCrumbs(Boolean use) {
|
||||
useCrumbs = use;
|
||||
}
|
||||
|
||||
public synchronized void doTestPost( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
|
||||
JSONObject form = req.getSubmittedForm();
|
||||
rsp.sendRedirect("foo");
|
||||
|
|
@ -2608,6 +2650,9 @@ public final class Hudson extends Node implements ItemGroup<TopLevelItem>, Stapl
|
|||
public void doDoFingerprintCheck( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
|
||||
// Parse the request
|
||||
MultipartFormDataParser p = new MultipartFormDataParser(req);
|
||||
if(Hudson.getInstance().isUseCrumbs() && !Hudson.getInstance().getCrumbIssuer().validateCrumb(req, p)) {
|
||||
rsp.sendError(HttpServletResponse.SC_FORBIDDEN,"No crumb found");
|
||||
}
|
||||
try {
|
||||
rsp.sendRedirect2(req.getContextPath()+"/fingerprint/"+
|
||||
Util.getDigestOf(p.getFileItem("name").getInputStream())+'/');
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ import org.tmatesoft.svn.core.wc.SVNWCUtil;
|
|||
import org.tmatesoft.svn.core.wc.SVNLogClient;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.BufferedReader;
|
||||
|
|
@ -1286,6 +1287,10 @@ public class SubversionSCM extends SCM implements Serializable {
|
|||
|
||||
MultipartFormDataParser parser = new MultipartFormDataParser(req);
|
||||
|
||||
if(Hudson.getInstance().isUseCrumbs() && !Hudson.getInstance().getCrumbIssuer().validateCrumb(req, parser)) {
|
||||
rsp.sendError(HttpServletResponse.SC_FORBIDDEN,"No crumb found");
|
||||
}
|
||||
|
||||
String url = parser.get("url");
|
||||
|
||||
String kind = parser.get("kind");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
/**
|
||||
* Copyright (c) 2008-2009 Yahoo! Inc.
|
||||
* All rights reserved.
|
||||
* The copyrights to the contents of this file are licensed under the MIT License (http://www.opensource.org/licenses/mit-license.php)
|
||||
*/
|
||||
package hudson.security.csrf;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* Checks for and validates crumbs on requests that cause state changes, to
|
||||
* protect against cross site request forgeries.
|
||||
*
|
||||
* @author dty
|
||||
*
|
||||
*/
|
||||
public class CrumbFilter implements Filter {
|
||||
|
||||
private volatile CrumbIssuer crumbIssuer;
|
||||
|
||||
public CrumbIssuer getCrumbIssuer() {
|
||||
return crumbIssuer;
|
||||
}
|
||||
|
||||
public void setCrumbIssuer(CrumbIssuer issuer) {
|
||||
crumbIssuer = issuer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link CrumbFilter} created for the given {@link ServletContext}.
|
||||
*/
|
||||
public static CrumbFilter get(ServletContext context) {
|
||||
return (CrumbFilter) context.getAttribute(CrumbFilter.class.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
// this is how we make us available to the rest of Hudson.
|
||||
filterConfig.getServletContext().setAttribute(CrumbFilter.class.getName(), this);
|
||||
}
|
||||
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
if (crumbIssuer == null) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
if (!(request instanceof HttpServletRequest)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
String crumbFieldName = crumbIssuer.getDescriptor().getCrumbRequestField();
|
||||
String crumbSalt = crumbIssuer.getDescriptor().getCrumbSalt();
|
||||
|
||||
if ("POST".equals(httpRequest.getMethod())) {
|
||||
String crumb = httpRequest.getHeader(crumbFieldName);
|
||||
boolean valid = false;
|
||||
if (crumb == null) {
|
||||
Enumeration<?> paramNames = request.getParameterNames();
|
||||
while (paramNames.hasMoreElements()) {
|
||||
String paramName = (String) paramNames.nextElement();
|
||||
if (crumbFieldName.equals(paramName)) {
|
||||
crumb = request.getParameter(paramName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (crumb != null) {
|
||||
if (crumbIssuer.validateCrumb(httpRequest, crumbSalt, crumb)) {
|
||||
valid = true;
|
||||
} else {
|
||||
LOGGER.warning("Found invalid crumb " + crumb +
|
||||
". Will check remaining parameters for a valid one...");
|
||||
}
|
||||
}
|
||||
// Multipart requests need to be handled by each handler.
|
||||
if (valid || isMultipart(httpRequest)) {
|
||||
chain.doFilter(request, response);
|
||||
} else {
|
||||
LOGGER.warning("No valid crumb was included in request for " + httpRequest.getRequestURI() + ". Returning " + HttpServletResponse.SC_FORBIDDEN + ".");
|
||||
((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN);
|
||||
}
|
||||
} else {
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
protected static boolean isMultipart(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String contentType = request.getContentType();
|
||||
if (contentType == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] parts = contentType.split(";");
|
||||
if (parts.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
if ("multipart/form-data".equals(parts[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public void destroy() {
|
||||
}
|
||||
private static final Logger LOGGER = Logger.getLogger(CrumbFilter.class.getName());
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
/**
|
||||
* Copyright (c) 2008-2009 Yahoo! Inc.
|
||||
* All rights reserved.
|
||||
* The copyrights to the contents of this file are licensed under the MIT License (http://www.opensource.org/licenses/mit-license.php)
|
||||
*/
|
||||
package hudson.security.csrf;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
|
||||
import org.kohsuke.stapler.Stapler;
|
||||
import org.kohsuke.stapler.export.Exported;
|
||||
import org.kohsuke.stapler.export.ExportedBean;
|
||||
|
||||
import hudson.DescriptorExtensionList;
|
||||
import hudson.ExtensionPoint;
|
||||
import hudson.model.Api;
|
||||
import hudson.model.Describable;
|
||||
import hudson.model.Descriptor;
|
||||
import hudson.model.Hudson;
|
||||
import hudson.util.MultipartFormDataParser;
|
||||
|
||||
/**
|
||||
* A CrumbIssuer represents an algorithm to generate a nonce value, known as a
|
||||
* crumb, to counter cross site request forgery exploits. Crumbs are typically
|
||||
* hashes incorporating information that uniquely identifies an agent that sends
|
||||
* a request, along with a guarded secret so that the crumb value cannot be
|
||||
* forged by a third party.
|
||||
*
|
||||
* @author dty
|
||||
* @see http://en.wikipedia.org/wiki/XSRF
|
||||
*/
|
||||
@ExportedBean
|
||||
public abstract class CrumbIssuer implements Describable<CrumbIssuer>, ExtensionPoint {
|
||||
|
||||
private static final String CRUMB_ATTRIBUTE = CrumbIssuer.class.getName() + "_crumb";
|
||||
|
||||
/**
|
||||
* Get the name of the request parameter the crumb will be stored in. Exposed
|
||||
* here for the remote API.
|
||||
*/
|
||||
@Exported
|
||||
public String getCrumbRequestField() {
|
||||
return getDescriptor().getCrumbRequestField();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a crumb value based on user specific information in the current request.
|
||||
* Intended for use only by the remote API.
|
||||
* @return
|
||||
*/
|
||||
@Exported
|
||||
public String getCrumb() {
|
||||
return getCrumb(Stapler.getCurrentRequest());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a crumb value based on user specific information in the request.
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
public String getCrumb(ServletRequest request) {
|
||||
String crumb = null;
|
||||
if (request != null) {
|
||||
crumb = (String) request.getAttribute(CRUMB_ATTRIBUTE);
|
||||
}
|
||||
if (crumb == null) {
|
||||
crumb = issueCrumb(request, getDescriptor().getCrumbSalt());
|
||||
if (request != null) {
|
||||
if ((crumb != null) && !crumb.isEmpty()) {
|
||||
request.setAttribute(CRUMB_ATTRIBUTE, crumb);
|
||||
} else {
|
||||
request.removeAttribute(CRUMB_ATTRIBUTE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return crumb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a crumb value based on user specific information in the request.
|
||||
* The crumb should be generated by building a cryptographic hash of:
|
||||
* <ul>
|
||||
* <li>relevant information in the request that can uniquely identify the client
|
||||
* <li>the salt value
|
||||
* <li>an implementation specific guarded secret.
|
||||
* </ul>
|
||||
*
|
||||
* @param request
|
||||
* @param salt
|
||||
* @return
|
||||
*/
|
||||
protected abstract String issueCrumb(ServletRequest request, String salt);
|
||||
|
||||
/**
|
||||
* Get a crumb from a request parameter and validate it against other data
|
||||
* in the current request. The salt and request parameter that is used is
|
||||
* defined by the current configuration.
|
||||
*
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
public boolean validateCrumb(ServletRequest request) {
|
||||
CrumbIssuerDescriptor<CrumbIssuer> desc = getDescriptor();
|
||||
String crumbField = desc.getCrumbRequestField();
|
||||
String crumbSalt = desc.getCrumbSalt();
|
||||
|
||||
return validateCrumb(request, crumbSalt, request.getParameter(crumbField));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a crumb from multipart form data and validate it against other data
|
||||
* in the current request. The salt and request parameter that is used is
|
||||
* defined by the current configuration.
|
||||
*
|
||||
* @param request
|
||||
* @param parser
|
||||
* @return
|
||||
*/
|
||||
public boolean validateCrumb(ServletRequest request, MultipartFormDataParser parser) {
|
||||
CrumbIssuerDescriptor<CrumbIssuer> desc = getDescriptor();
|
||||
String crumbField = desc.getCrumbRequestField();
|
||||
String crumbSalt = desc.getCrumbSalt();
|
||||
|
||||
return validateCrumb(request, crumbSalt, parser.get(crumbField));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a previously created crumb against information in the current request.
|
||||
*
|
||||
* @param request
|
||||
* @param salt
|
||||
* @param crumb The previously generated crumb to validate against information in the current request
|
||||
* @return
|
||||
*/
|
||||
public abstract boolean validateCrumb(ServletRequest request, String salt, String crumb);
|
||||
|
||||
/**
|
||||
* Access global configuration for the crumb issuer.
|
||||
*/
|
||||
public CrumbIssuerDescriptor<CrumbIssuer> getDescriptor() {
|
||||
return (CrumbIssuerDescriptor<CrumbIssuer>) Hudson.getInstance().getDescriptor(getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the registered {@link CrumbIssuer} descriptors.
|
||||
*/
|
||||
public static DescriptorExtensionList<CrumbIssuer, Descriptor<CrumbIssuer>> all() {
|
||||
return Hudson.getInstance().getDescriptorList(CrumbIssuer.class);
|
||||
}
|
||||
|
||||
public Api getApi() {
|
||||
return new Api(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* Copyright (c) 2008-2009 Yahoo! Inc.
|
||||
* All rights reserved.
|
||||
* The copyrights to the contents of this file are licensed under the MIT License (http://www.opensource.org/licenses/mit-license.php)
|
||||
*/
|
||||
package hudson.security.csrf;
|
||||
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
|
||||
import hudson.Util;
|
||||
import hudson.model.Descriptor;
|
||||
|
||||
/**
|
||||
* Describes global configuration for crumb issuers. Create subclasses to specify
|
||||
* additional global configuration for custom crumb issuers.
|
||||
*
|
||||
* @author dty
|
||||
*
|
||||
*/
|
||||
public abstract class CrumbIssuerDescriptor<T extends CrumbIssuer> extends Descriptor<CrumbIssuer> {
|
||||
|
||||
private String crumbSalt;
|
||||
private String crumbRequestField;
|
||||
|
||||
/**
|
||||
* Crumb issuers always take a salt and a request field name.
|
||||
*
|
||||
* @param salt Salt value
|
||||
* @param crumbRequestField Request parameter name containing crumb from previous response
|
||||
*/
|
||||
protected CrumbIssuerDescriptor(String salt, String crumbRequestField) {
|
||||
setCrumbSalt(salt);
|
||||
setCrumbRequestField(crumbRequestField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the salt value.
|
||||
* @return
|
||||
*/
|
||||
public String getCrumbSalt() {
|
||||
return crumbSalt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the salt value. Must not be null.
|
||||
* @param salt
|
||||
*/
|
||||
public void setCrumbSalt(String salt) {
|
||||
if (Util.fixEmptyAndTrim(salt) == null) {
|
||||
crumbSalt = "hudson.crumb";
|
||||
} else {
|
||||
crumbSalt = salt;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request parameter name that contains the crumb generated from a
|
||||
* previous response.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getCrumbRequestField() {
|
||||
return crumbRequestField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the request parameter name. Must not be null.
|
||||
*
|
||||
* @param requestField
|
||||
*/
|
||||
public void setCrumbRequestField(String requestField) {
|
||||
if (Util.fixEmptyAndTrim(requestField) == null) {
|
||||
crumbRequestField = ".crumb";
|
||||
} else {
|
||||
crumbRequestField = requestField;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configure(StaplerRequest request) {
|
||||
setCrumbSalt(request.getParameter("csrf_crumbSalt"));
|
||||
setCrumbRequestField(request.getParameter("csrf_crumbRequestField"));
|
||||
save();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Jelly script that contains common configuration.
|
||||
*/
|
||||
@Override
|
||||
public final String getConfigPage() {
|
||||
return getViewPage(CrumbIssuer.class, "config.jelly");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a subclass specific configuration page. The base CrumbIssuerDescriptor
|
||||
* class provides configuration options that are common to all crumb issuers.
|
||||
* Implementations may provide additional configuration options which are
|
||||
* kept in Jelly script file tied to the subclass.
|
||||
* <p>
|
||||
* By default, an empty string is returned, which signifies no additional
|
||||
* configuration is needed for a crumb issuer. Override this method if your
|
||||
* crumb issuer has additional configuration options.
|
||||
* <p>
|
||||
* A typical implementation of this method would look like:
|
||||
* <p>
|
||||
* <code>
|
||||
* return getViewPage(clazz, "config.jelly");
|
||||
* </code>
|
||||
*
|
||||
* @return An empty string, signifying no additional configuration.
|
||||
*/
|
||||
public String getSubConfigPage() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* Copyright (c) 2008-2009 Yahoo! Inc.
|
||||
* All rights reserved.
|
||||
* The copyrights to the contents of this file are licensed under the MIT License (http://www.opensource.org/licenses/mit-license.php)
|
||||
*/
|
||||
package hudson.security.csrf;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import hudson.Extension;
|
||||
import hudson.model.Hudson;
|
||||
import hudson.model.ModelObject;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import net.sf.json.JSONObject;
|
||||
|
||||
import org.acegisecurity.Authentication;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
|
||||
/**
|
||||
* A crumb issuing algorithm based on the request principal and the remote address.
|
||||
*
|
||||
* @author dty
|
||||
*
|
||||
*/
|
||||
public class DefaultCrumbIssuer extends CrumbIssuer {
|
||||
|
||||
private MessageDigest md;
|
||||
|
||||
DefaultCrumbIssuer() {
|
||||
try {
|
||||
this.md = MessageDigest.getInstance("MD5");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
this.md = null;
|
||||
LOGGER.log(Level.SEVERE, "Can't find MD5", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected String issueCrumb(ServletRequest request, String salt) {
|
||||
if (request instanceof HttpServletRequest) {
|
||||
if (md != null) {
|
||||
HttpServletRequest req = (HttpServletRequest) request;
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
Authentication a = Hudson.getAuthentication();
|
||||
if (a != null) {
|
||||
buffer.append(a.getName());
|
||||
}
|
||||
buffer.append(';');
|
||||
buffer.append(req.getRemoteAddr());
|
||||
|
||||
md.update(buffer.toString().getBytes());
|
||||
md.update(salt.getBytes());
|
||||
byte[] crumbBytes = md.digest(Hudson.getInstance().getSecretKey().getBytes());
|
||||
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (int i = 0; i < crumbBytes.length; i++) {
|
||||
String hex = Integer.toHexString(0xFF & crumbBytes[i]);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean validateCrumb(ServletRequest request, String salt, String crumb) {
|
||||
if (request instanceof HttpServletRequest) {
|
||||
String newCrumb = issueCrumb(request, salt);
|
||||
if ((newCrumb != null) && (crumb != null)) {
|
||||
return newCrumb.equals(crumb);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Extension
|
||||
public static final class DescriptorImpl extends CrumbIssuerDescriptor<DefaultCrumbIssuer> implements ModelObject {
|
||||
|
||||
public DescriptorImpl() {
|
||||
super(null, null);
|
||||
load();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Default Crumb Issuer";
|
||||
}
|
||||
|
||||
public DefaultCrumbIssuer newInstance(StaplerRequest req, JSONObject formData) throws FormException {
|
||||
return new DefaultCrumbIssuer();
|
||||
}
|
||||
}
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(DefaultCrumbIssuer.class.getName());
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!--
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Seiji Sogabe
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Seiji Sogabe, Yahoo! 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
|
||||
|
|
@ -110,6 +110,9 @@ THE SOFTWARE.
|
|||
|
||||
<!-- trigger -->
|
||||
new Ajax.Request(btn.getAttribute('url')+"/make"+(btn.checked?"Enable":"Disable")+"d", {
|
||||
<j:if test="${app.useCrumbs}">
|
||||
parameters : { "${h.getCrumbRequestField()}": "${h.getCrumb(request)}" },
|
||||
</j:if>
|
||||
onFailure : function(req,o) {
|
||||
$('needRestart').innerHTML = req.responseText;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!--
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt, Tom Huybrechts, id:cactusman
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt, Tom Huybrechts, id:cactusman, Yahoo! 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
|
||||
|
|
@ -46,7 +46,8 @@ THE SOFTWARE.
|
|||
onclick="${it.parameterized?null:'return build(this)'}" permission="${it.BUILD}" />
|
||||
<script>
|
||||
function build(a) {
|
||||
new Ajax.Request(a.href,{method:"post"});
|
||||
new Ajax.Request(a.href,{method:"post",${h.getCrumbAsJSONParameterBlock(request)} });
|
||||
|
||||
hoverNotification('${%Build scheduled}',a.parentNode);
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!--
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt, Jean-Baptiste Quenot, Stephen Connolly, Tom Huybrechts
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt, Jean-Baptiste Quenot, Stephen Connolly, Tom Huybrechts, Yahoo! 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
|
||||
|
|
@ -88,6 +88,19 @@ THE SOFTWARE.
|
|||
</f:entry>
|
||||
</f:optionalBlock>
|
||||
|
||||
<j:if test="${!empty(h.crumbIssuerDescriptors)}">
|
||||
<f:optionalBlock name="csrf" title="${%Prevent Cross Site Request Forgery exploits}"
|
||||
checked="${it.useCrumbs}" help="/help/system-config/csrf.html">
|
||||
<f:entry title="${Crumbs}">
|
||||
<table style="width:100%">
|
||||
<f:descriptorRadioList title="${%Crumb Algorithm}" varName="issuer"
|
||||
instance="${it.crumbIssuer}"
|
||||
descriptors="${h.crumbIssuerDescriptors}"/>
|
||||
</table>
|
||||
</f:entry>
|
||||
</f:optionalBlock>
|
||||
</j:if>
|
||||
|
||||
<f:optionalBlock name="usageStatisticsCollected" checked="${it.usageStatisticsCollected}"
|
||||
title="${%statsBlurb}"
|
||||
help="/help/system-config/usage-statistics.html" />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!--
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, id:cactusman
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, id:cactusman, Yahoo! 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
|
||||
|
|
@ -55,6 +55,7 @@ THE SOFTWARE.
|
|||
</j:forEach>
|
||||
|
||||
<f:block>
|
||||
<f:crumb />
|
||||
<input type="submit" name="Submit" value="OK" id="ok" style="margin-left:5em" />
|
||||
</f:block>
|
||||
</f:form>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ THE SOFTWARE.
|
|||
updateCenter.postBackURL = "${rootURL}/updateCenter/postBack";
|
||||
updateCenter.info = { version:"${h.version}" };
|
||||
updateCenter.url = "${h.updateCenterUrl}";
|
||||
updateCenter.crumbName = "${h.getCrumbRequestField()}";
|
||||
updateCenter.crumb = "${h.getCrumb(request)}";
|
||||
Behaviour.addLoadEvent(updateCenter.checkUpdates);
|
||||
</script>
|
||||
</j:if>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!--
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Seiji Sogabe
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Seiji Sogabe, Yahoo! 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
|
||||
|
|
@ -47,6 +47,7 @@ THE SOFTWARE.
|
|||
window.setTimeout(function() {
|
||||
new Ajax.Request("./body", {
|
||||
method: "post",
|
||||
${h.getCrumbAsJSONParameterBlock(request)},
|
||||
onSuccess: function(rsp) {
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = rsp.responseText;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
<!--
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2008-2009, Yahoo! 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.
|
||||
-->
|
||||
|
||||
<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">
|
||||
<f:entry title="${%Salt}" help="/help/security/csrf/salt.html">
|
||||
<f:textbox name="csrf_crumbSalt" value="${descriptor.crumbSalt}" />
|
||||
</f:entry>
|
||||
<f:entry title="${%Request Field}" help="/help/security/csrf/field.html">
|
||||
<f:textbox name="csrf_crumbField" value="${descriptor.crumbRequestField}" />
|
||||
</f:entry>
|
||||
|
||||
<j:if test="${!empty(descriptor.subConfigPage)}">
|
||||
<st:include from="${descriptor}" page="${descriptor.subConfigPage}" optional="true" />
|
||||
</j:if>
|
||||
</j:jelly>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!--
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Seiji Sogabe, id:cactusman
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Seiji Sogabe, id:cactusman, Yahoo! 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
|
||||
|
|
@ -83,6 +83,6 @@ THE SOFTWARE.
|
|||
</tr>
|
||||
</l:pane>
|
||||
<script defer="true">
|
||||
updateBuildHistory("${it.baseUrl}/buildHistory/ajax",${it.nextBuildNumberToFetch});
|
||||
updateBuildHistory("${it.baseUrl}/buildHistory/ajax",${it.nextBuildNumberToFetch},"${h.getCrumbRequestField()}","${h.getCrumb(request)}");
|
||||
</script>
|
||||
</j:jelly>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<!--
|
||||
Copyright (c) 2008-2009 Yahoo! Inc.
|
||||
All rights reserved.
|
||||
The copyrights to the contents of this file are licensed under the MIT License (http://www.opensource.org/licenses/mit-license.php)
|
||||
-->
|
||||
|
||||
<j:jelly xmlns:j="jelly:core" xmlns:s="jelly:stapler" xmlns:d="jelly:define" xmlns:f="/lib/form">
|
||||
<j:if test="${app.useCrumbs}">
|
||||
<input type="hidden" name="${h.getCrumbRequestField()}" value="${h.getCrumb(request)}" />
|
||||
</j:if>
|
||||
</j:jelly>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!--
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! 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
|
||||
|
|
@ -22,7 +22,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|||
THE SOFTWARE.
|
||||
-->
|
||||
|
||||
<j:jelly xmlns:j="jelly:core" xmlns:s="jelly:stapler" xmlns:d="jelly:define">
|
||||
<j:jelly xmlns:j="jelly:core" xmlns:s="jelly:stapler" xmlns:d="jelly:define" xmlns:f="/lib/form">
|
||||
<s:documentation>
|
||||
Submit button themed by YUI. This should be always
|
||||
used instead of the plain <input tag.
|
||||
|
|
@ -36,5 +36,6 @@ THE SOFTWARE.
|
|||
The text of the submit button. Something like "submit", "OK", etc.
|
||||
</s:attribute>
|
||||
</s:documentation>
|
||||
<f:crumb/>
|
||||
<input type="submit" name="${attrs.name ?: 'Submit'}" value="${attrs.value}" class="submit-button" />
|
||||
</j:jelly>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!--
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Tom Huybrechts
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Tom Huybrechts, Yahoo! 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
|
||||
|
|
@ -39,7 +39,7 @@ THE SOFTWARE.
|
|||
</div>
|
||||
|
||||
<l:hasPermission permission="${permission}">
|
||||
<div align="right"><a href="editDescription" onclick="${h.isAutoRefresh(request) ? null : 'return replaceDescription();'}">
|
||||
<div align="right"><a href="editDescription" onclick="${h.getReplaceDescriptionInvoker(request)}">
|
||||
<img src="${imagesURL}/16x16/notepad.gif" alt="" />
|
||||
<j:choose>
|
||||
<j:when test="${empty(it.description)}">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!--
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Stephen Connolly, id:cactusman
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Stephen Connolly, id:cactusman, Yahoo! 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
|
||||
|
|
@ -120,7 +120,7 @@ THE SOFTWARE.
|
|||
<!-- schedule updates only for the full page reload -->
|
||||
<j:if test="${ajax==null and !h.isAutoRefresh(request) and h.hasPermission(app.READ)}">
|
||||
<script defer="defer">
|
||||
refreshPart('executors',"${h.hasView(it,'ajaxExecutors')?'.':rootURL}/ajaxExecutors");
|
||||
refreshPart('executors',"${h.hasView(it,'ajaxExecutors')?'.':rootURL}/ajaxExecutors", "${h.getCrumbRequestField()}", "${h.getCrumb(request)}");
|
||||
</script>
|
||||
</j:if>
|
||||
</l:pane>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!--
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! 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
|
||||
|
|
@ -69,6 +69,7 @@ THE SOFTWARE.
|
|||
-->
|
||||
<input type="radio" name="mode" value="dummy1" style="display:none" />
|
||||
<input type="radio" name="mode" value="dummy2" style="display:none" />
|
||||
<s:crumb />
|
||||
<input type="submit" name="Submit" value="OK" id="ok" style="margin-left:5em" />
|
||||
</s:block>
|
||||
</s:form>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!--
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! 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
|
||||
|
|
@ -44,7 +44,7 @@ THE SOFTWARE.
|
|||
function fetchNext(e,href) {
|
||||
new Ajax.Request(href,{
|
||||
method: "post",
|
||||
parameters: "start="+e.fetchedBytes,
|
||||
parameters: {"start":e.fetchedBytes,"${h.getCrumbRequestField()}":"${h.getCrumb(request)}"},
|
||||
onComplete: function(rsp,_) {
|
||||
<!-- append text and do autoscroll if applicable-->
|
||||
var stickToBottom = scroller.isSticking();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!--
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! 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
|
||||
|
|
@ -94,7 +94,7 @@ THE SOFTWARE.
|
|||
</j:choose>
|
||||
<j:if test="${ajax==null and attrs.autoRefresh and !h.isAutoRefresh(request)}">
|
||||
<script defer="defer">
|
||||
refreshPart('matrix',"./ajaxMatrix");
|
||||
refreshPart('matrix',"./ajaxMatrix","${h.getCrumbRequestField()}","${h.getCrumb(request)}");
|
||||
</script>
|
||||
</j:if>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!--
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Tom Huybrechts
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Tom Huybrechts, Yahoo! 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
|
||||
|
|
@ -82,7 +82,7 @@ THE SOFTWARE.
|
|||
<!-- schedule updates only for the full page reload -->
|
||||
<j:if test="${ajax==null and !h.isAutoRefresh(request) and h.hasPermission(app.READ)}">
|
||||
<script defer="defer">
|
||||
refreshPart('buildQueue',"${h.hasView(it,'ajaxBuildQueue')?'.':rootURL}/ajaxBuildQueue");
|
||||
refreshPart('buildQueue',"${h.hasView(it,'ajaxBuildQueue')?'.':rootURL}/ajaxBuildQueue", "${h.getCrumbRequestField()}", "${h.getCrumb(request)}");
|
||||
</script>
|
||||
</j:if>
|
||||
</l:pane>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ import hudson.model.AbstractProject;
|
|||
import hudson.model.UpdateCenter.UpdateCenterConfiguration;
|
||||
import hudson.model.Node.Mode;
|
||||
import hudson.scm.SubversionSCM;
|
||||
import hudson.security.csrf.CrumbIssuer;
|
||||
import hudson.security.csrf.CrumbIssuerDescriptor;
|
||||
import hudson.slaves.CommandLauncher;
|
||||
import hudson.slaves.DumbSlave;
|
||||
import hudson.slaves.RetentionStrategy;
|
||||
|
|
@ -98,6 +100,7 @@ import javax.servlet.ServletContextEvent;
|
|||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.apache.commons.httpclient.NameValuePair;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.beanutils.PropertyUtils;
|
||||
|
|
@ -128,8 +131,10 @@ import org.xml.sax.SAXException;
|
|||
import com.gargoylesoftware.htmlunit.AjaxController;
|
||||
import com.gargoylesoftware.htmlunit.BrowserVersion;
|
||||
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
|
||||
import com.gargoylesoftware.htmlunit.HttpMethod;
|
||||
import com.gargoylesoftware.htmlunit.Page;
|
||||
import com.gargoylesoftware.htmlunit.WebRequestSettings;
|
||||
import com.gargoylesoftware.htmlunit.html.DomNode;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlButton;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlForm;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||
|
|
@ -199,6 +204,10 @@ public abstract class HudsonTestCase extends TestCase {
|
|||
|
||||
hudson = newHudson();
|
||||
hudson.setNoUsageStatistics(true); // collecting usage stats from tests are pointless.
|
||||
|
||||
hudson.setUseCrumbs(true);
|
||||
hudson.setCrumbIssuer(((CrumbIssuerDescriptor<CrumbIssuer>)Hudson.getInstance().getDescriptor(TestCrumbIssuer.class)).newInstance(null,null));
|
||||
|
||||
hudson.servletContext.setAttribute("app",hudson);
|
||||
hudson.servletContext.setAttribute("version","?");
|
||||
WebAppMain.installExpressionFactory(new ServletContextEvent(hudson.servletContext));
|
||||
|
|
@ -918,6 +927,30 @@ public abstract class HudsonTestCase extends TestCase {
|
|||
public String getContextPath() {
|
||||
return "http://localhost:"+localPort+contextPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a security crumb to the quest
|
||||
*/
|
||||
public WebRequestSettings addCrumb(WebRequestSettings req) {
|
||||
NameValuePair crumb[] = { new NameValuePair() };
|
||||
|
||||
crumb[0].setName(hudson.getCrumbIssuer().getDescriptor().getCrumbRequestField());
|
||||
crumb[0].setValue(hudson.getCrumbIssuer().getCrumb( null ));
|
||||
|
||||
req.setRequestParameters(Arrays.asList( crumb ));
|
||||
return req;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a URL with crumb parameters relative to {{@link #getContextPath()}
|
||||
*/
|
||||
public URL createCrumbedUrl(String relativePath) throws MalformedURLException {
|
||||
CrumbIssuer issuer = hudson.getCrumbIssuer();
|
||||
String crumbName = issuer.getDescriptor().getCrumbRequestField();
|
||||
String crumb = issuer.getCrumb(null);
|
||||
|
||||
return new URL(getContextPath()+relativePath+"?"+crumbName+"="+crumb);
|
||||
}
|
||||
}
|
||||
|
||||
// needs to keep reference, or it gets GC-ed.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* Copyright (c) 2008-2009 Yahoo! Inc.
|
||||
* All rights reserved.
|
||||
* The copyrights to the contents of this file are licensed under the MIT License (http://www.opensource.org/licenses/mit-license.php)
|
||||
*/
|
||||
package org.jvnet.hudson.test;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
|
||||
import net.sf.json.JSONObject;
|
||||
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
|
||||
import hudson.Extension;
|
||||
import hudson.model.ModelObject;
|
||||
import hudson.security.csrf.CrumbIssuer;
|
||||
import hudson.security.csrf.CrumbIssuerDescriptor;
|
||||
|
||||
/**
|
||||
* A crumb issuer that issues a constant crumb value. Used for unit testing.
|
||||
* @author dty
|
||||
*/
|
||||
public class TestCrumbIssuer extends CrumbIssuer
|
||||
{
|
||||
@Override
|
||||
protected String issueCrumb( ServletRequest request, String salt )
|
||||
{
|
||||
return "test";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateCrumb( ServletRequest request, String salt, String crumb )
|
||||
{
|
||||
return "test".equals(crumb);
|
||||
}
|
||||
|
||||
@Extension
|
||||
public static final class DescriptorImpl extends CrumbIssuerDescriptor<TestCrumbIssuer> implements ModelObject {
|
||||
public DescriptorImpl()
|
||||
{
|
||||
super(null, null);
|
||||
load();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Test Crumb";
|
||||
}
|
||||
|
||||
public TestCrumbIssuer newInstance(StaplerRequest req, JSONObject formData) throws FormException {
|
||||
return new TestCrumbIssuer();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
|
||||
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! 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
|
||||
|
|
@ -43,7 +43,7 @@ import org.jvnet.hudson.test.Email;
|
|||
import org.jvnet.hudson.test.HudsonTestCase;
|
||||
import org.jvnet.hudson.test.recipes.LocalData;
|
||||
|
||||
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ public class HudsonTest extends HudsonTestCase {
|
|||
wc.getPage(req);
|
||||
fail("Error code expected");
|
||||
} catch (FailingHttpStatusCodeException e) {
|
||||
assertEquals(SC_BAD_REQUEST,e.getStatusCode());
|
||||
assertEquals(SC_FORBIDDEN,e.getStatusCode());
|
||||
}
|
||||
|
||||
// the master computer object should be still here
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
|
||||
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! 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
|
||||
|
|
@ -54,8 +54,8 @@ public class ExpandableTextboxTest extends HudsonTestCase {
|
|||
*/
|
||||
protected HtmlPage evaluateAsHtml(String jellyScript) throws Exception {
|
||||
HudsonTestCase.WebClient wc = new WebClient();
|
||||
|
||||
WebRequestSettings req = new WebRequestSettings(new URL(wc.getContextPath()+"eval"), POST);
|
||||
|
||||
WebRequestSettings req = new WebRequestSettings(wc.createCrumbedUrl("eval"), POST);
|
||||
req.setRequestBody("<j:jelly xmlns:j='jelly:core' xmlns:st='jelly:stapler' xmlns:l='/lib/layout' xmlns:f='/lib/form'>"+jellyScript+"</j:jelly>");
|
||||
Page page = wc.getPage(req);
|
||||
return (HtmlPage) page;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<!--
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Tom Huybrechts, id:digerata
|
||||
Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Tom Huybrechts, id:digerata, Yahoo! 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
|
||||
|
|
@ -49,6 +49,11 @@ THE SOFTWARE.
|
|||
<filter-class>hudson.security.HudsonFilter</filter-class>
|
||||
</filter>
|
||||
|
||||
<filter>
|
||||
<filter-name>csrf-filter</filter-name>
|
||||
<filter-class>hudson.security.csrf.CrumbFilter</filter-class>
|
||||
</filter>
|
||||
|
||||
<!--
|
||||
The Headers filter allows us to to override headers sent by the container
|
||||
that may be in conflict with what we want. For example, Tomcat will set
|
||||
|
|
@ -82,7 +87,11 @@ THE SOFTWARE.
|
|||
<filter-name>authentication-filter</filter-name>
|
||||
<url-pattern>/*</url-pattern>
|
||||
</filter-mapping>
|
||||
|
||||
<filter-mapping>
|
||||
<filter-name>csrf-filter</filter-name>
|
||||
<url-pattern>/*</url-pattern>
|
||||
</filter-mapping>
|
||||
|
||||
<filter>
|
||||
<filter-name>plugins-filter</filter-name>
|
||||
<filter-class>hudson.util.PluginServletFilter</filter-class>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
<div>
|
||||
This is name of the request parameter Hudson will look in for a crumb
|
||||
value.
|
||||
</div>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<div>
|
||||
The salt value is taken as an input to the crumb generation algorithm. It acts
|
||||
as further randomization to complicate dictionary style attacks against the
|
||||
algorithm. In the context of CSRF exploits against Hudson servers, each Hudson
|
||||
server should use a different salt value. If multiple Hudson servers all use
|
||||
a crumb generation algorithm that gets broken, the salt prevents an attacker
|
||||
from running CSRF exploits against all these servers.
|
||||
</div>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<div>
|
||||
A cross site request forgery (or CSRF/XSRF) is an exploit that enables
|
||||
an unauthorized third party to take actions on a web site as you. In Hudson,
|
||||
this could allow someone to delete jobs, builds or change Hudson's configuration.
|
||||
<p>
|
||||
When this option is enabled, Hudson will check for a generated nonce value, or
|
||||
"crumb", on any request that may cause a change on the Hudson server. This
|
||||
includes any form submission and calls to the remote API.
|
||||
<p>
|
||||
More information about CSRF exploits can be found <a href="http://www.owasp.org/index.php/Cross-Site_Request_Forgery">here</a>.
|
||||
</div>
|
||||
|
|
@ -623,13 +623,16 @@ function xor(a,b) {
|
|||
}
|
||||
|
||||
// used by editableDescription.jelly to replace the description field with a form
|
||||
function replaceDescription() {
|
||||
function replaceDescription(crumbName,crumb) {
|
||||
var d = document.getElementById("description");
|
||||
d.firstChild.nextSibling.innerHTML = "<div class='spinner-right'>loading...</div>";
|
||||
var params = new Array(1);
|
||||
params[crumbName] = crumb;
|
||||
new Ajax.Request(
|
||||
"./descriptionForm",
|
||||
{
|
||||
method : 'post',
|
||||
parameters : params,
|
||||
onComplete : function(x) {
|
||||
d.innerHTML = x.responseText;
|
||||
Behaviour.applySubtree(d);
|
||||
|
|
@ -803,10 +806,13 @@ function expandTextArea(button,id) {
|
|||
|
||||
// refresh a part of the HTML specified by the given ID,
|
||||
// by using the contents fetched from the given URL.
|
||||
function refreshPart(id,url) {
|
||||
function refreshPart(id,url,crumbName,crumb) {
|
||||
var f = function() {
|
||||
var params = new Array(1);
|
||||
params[crumbName] = crumb;
|
||||
new Ajax.Request(url, {
|
||||
method: "post",
|
||||
parameters: params,
|
||||
onSuccess: function(rsp) {
|
||||
var hist = $(id);
|
||||
var p = hist.parentNode;
|
||||
|
|
@ -822,7 +828,7 @@ function refreshPart(id,url) {
|
|||
Behaviour.applySubtree(node);
|
||||
|
||||
if(isRunAsTest) return;
|
||||
refreshPart(id,url);
|
||||
refreshPart(id,url,crumbName,crumb);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -1074,14 +1080,18 @@ function addRadioBlock(id) {
|
|||
}
|
||||
|
||||
|
||||
function updateBuildHistory(ajaxUrl,nBuild) {
|
||||
function updateBuildHistory(ajaxUrl,nBuild,crumbName,crumb) {
|
||||
if(isRunAsTest) return;
|
||||
$('buildHistory').headers = ["n",nBuild];
|
||||
|
||||
function updateBuilds() {
|
||||
var bh = $('buildHistory');
|
||||
var params = new Array(1);
|
||||
params[crumbName] = crumb;
|
||||
new Ajax.Request(ajaxUrl, {
|
||||
requestHeaders: bh.headers,
|
||||
method: "post",
|
||||
parameters: params,
|
||||
onSuccess: function(rsp) {
|
||||
var rows = bh.rows;
|
||||
|
||||
|
|
@ -1111,9 +1121,12 @@ function updateBuildHistory(ajaxUrl,nBuild) {
|
|||
|
||||
// send async request to the given URL (which will send back serialized ListBoxModel object),
|
||||
// then use the result to fill the list box.
|
||||
function updateListBox(listBox,url) {
|
||||
function updateListBox(listBox,url,crumbName,crumb) {
|
||||
var params = new Array(1);
|
||||
params[crumbName] = crumb;
|
||||
new Ajax.Request(url, {
|
||||
method: "post",
|
||||
parameters: params,
|
||||
onSuccess: function(rsp) {
|
||||
var l = $(listBox);
|
||||
while(l.length>0) l.options[0] = null;
|
||||
|
|
@ -1500,6 +1513,8 @@ function loadScript(href) {
|
|||
|
||||
var downloadService = {
|
||||
continuations: {},
|
||||
crumbName: null,
|
||||
crumb: null,
|
||||
|
||||
download : function(id,url,info, postBack,completionHandler) {
|
||||
this.continuations[id] = {postBack:postBack,completionHandler:completionHandler};
|
||||
|
|
@ -1508,9 +1523,12 @@ var downloadService = {
|
|||
|
||||
post : function(id,data) {
|
||||
var o = this.continuations[id];
|
||||
var params = new Array(2);
|
||||
params["json"] = Object.toJSON(data);
|
||||
params[downloadService.crumbName] = downloadService.crumb;
|
||||
new Ajax.Request(o.postBack, {
|
||||
method:"post",
|
||||
parameters:{json:Object.toJSON(data)},
|
||||
parameters:params,
|
||||
onSuccess: function() {
|
||||
if(o.completionHandler!=null)
|
||||
o.completionHandler();
|
||||
|
|
@ -1525,6 +1543,8 @@ var updateCenter = {
|
|||
postBackURL : null,
|
||||
info: {},
|
||||
completionHandler: null,
|
||||
crumbName: null,
|
||||
crumb: null,
|
||||
url: "https://hudson.dev.java.net/",
|
||||
|
||||
checkUpdates : function() {
|
||||
|
|
@ -1532,9 +1552,12 @@ var updateCenter = {
|
|||
},
|
||||
|
||||
post : function(data) {
|
||||
var params = new Array(2);
|
||||
params["json"] = Object.toJSON(data);
|
||||
params[updateCenter.crumbName] = updateCenter.crumb;
|
||||
new Ajax.Request(updateCenter.postBackURL, {
|
||||
method:"post",
|
||||
parameters:{json:Object.toJSON(data)},
|
||||
parameters:params,
|
||||
onSuccess: function() {
|
||||
if(updateCenter.completionHandler!=null)
|
||||
updateCenter.completionHandler();
|
||||
|
|
|
|||
Loading…
Reference in New Issue