-
-
Notifications
You must be signed in to change notification settings - Fork 9.1k
[JENKINS-18114] Fixing CLI crumbs #3019
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
856cd92
13b4d74
587ebbe
652b392
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,7 @@ | ||
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.net.HttpURLConnection; | ||
import java.net.URL; | ||
|
@@ -16,7 +14,7 @@ | |
/** | ||
* Creates a capacity-unlimited bi-directional {@link InputStream}/{@link OutputStream} pair over | ||
* HTTP, which is a request/response protocol. | ||
* | ||
* {@code FullDuplexHttpService} is the counterpart on the server side. | ||
* @author Kohsuke Kawaguchi | ||
*/ | ||
public class FullDuplexHttpStream { | ||
|
@@ -29,10 +27,18 @@ public class FullDuplexHttpStream { | |
private final OutputStream output; | ||
private final InputStream input; | ||
|
||
/** | ||
* A way to get data from the server. | ||
* There will be an initial zero byte used as a handshake which you should expect and ignore. | ||
*/ | ||
public InputStream getInputStream() { | ||
return input; | ||
} | ||
|
||
/** | ||
* A way to upload data to the server. | ||
* You will need to write to this and {@link OutputStream#flush} it to finish establishing a connection. | ||
*/ | ||
public OutputStream getOutputStream() { | ||
return output; | ||
} | ||
|
@@ -79,11 +85,10 @@ public FullDuplexHttpStream(URL base, String relativeTarget, String authorizatio | |
|
||
URL target = new URL(this.base, relativeTarget); | ||
|
||
CrumbData crumbData = new CrumbData(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As described in PR comments, this did not previously work, and given the existence of an exclusion, was not necessary anyway. |
||
|
||
UUID uuid = UUID.randomUUID(); // so that the server can correlate those two connections | ||
|
||
// server->client | ||
LOGGER.fine("establishing download side"); | ||
HttpURLConnection con = (HttpURLConnection) target.openConnection(); | ||
con.setDoOutput(true); // request POST to avoid caching | ||
con.setRequestMethod("POST"); | ||
|
@@ -92,17 +97,16 @@ public FullDuplexHttpStream(URL base, String relativeTarget, String authorizatio | |
if (authorization != null) { | ||
con.addRequestProperty("Authorization", authorization); | ||
} | ||
if(crumbData.isValid) { | ||
con.addRequestProperty(crumbData.crumbName, crumbData.crumb); | ||
} | ||
con.getOutputStream().close(); | ||
input = con.getInputStream(); | ||
// make sure we hit the right URL | ||
// make sure we hit the right URL; no need for CLI.verifyJenkinsConnection here | ||
if (con.getHeaderField("Hudson-Duplex") == null) { | ||
throw new CLI.NotTalkingToJenkinsException("There's no Jenkins running at " + target + ", or is not serving the HTTP Duplex transport"); | ||
} | ||
LOGGER.fine("established download side"); // calling getResponseCode or getHeaderFields breaks everything | ||
|
||
// client->server uses chunked encoded POST for unlimited capacity. | ||
// client->server uses chunked encoded POST for unlimited capacity. | ||
LOGGER.fine("establishing upload side"); | ||
con = (HttpURLConnection) target.openConnection(); | ||
con.setDoOutput(true); // request POST | ||
con.setRequestMethod("POST"); | ||
|
@@ -113,11 +117,8 @@ public FullDuplexHttpStream(URL base, String relativeTarget, String authorizatio | |
if (authorization != null) { | ||
con.addRequestProperty ("Authorization", authorization); | ||
} | ||
|
||
if(crumbData.isValid) { | ||
con.addRequestProperty(crumbData.crumbName, crumbData.crumb); | ||
} | ||
output = con.getOutputStream(); | ||
LOGGER.fine("established upload side"); | ||
} | ||
|
||
// As this transport mode is using POST, it is necessary to resolve possible redirections using GET first. | ||
|
@@ -141,59 +142,4 @@ private URL tryToResolveRedirects(URL base, String authorization) { | |
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(); | ||
String[] pair = readData(base + "?xpath=concat(//crumbRequestField,\":\",//crumb)").split(":", 2); | ||
crumbName = pair[0]; | ||
crumb = pair[1]; | ||
isValid = true; | ||
LOGGER.fine("Crumb data: "+crumbName+"="+crumb); | ||
} catch (IOException e) { | ||
// presumably this Hudson doesn't use crumb | ||
LOGGER.log(Level.FINE,"Failed to get crumb data",e); | ||
} | ||
} | ||
|
||
private String createCrumbUrlBase() { | ||
return base + "crumbIssuer/api/xml/"; | ||
} | ||
|
||
private String readData(String dest) throws IOException { | ||
HttpURLConnection con = (HttpURLConnection) new URL(dest).openConnection(); | ||
if (authorization != null) { | ||
con.addRequestProperty("Authorization", authorization); | ||
} | ||
CLI.verifyJenkinsConnection(con); | ||
|
||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream()))) { | ||
String line = reader.readLine(); | ||
String nextLine = reader.readLine(); | ||
if (nextLine != null) { | ||
System.err.println("Warning: received junk from " + dest); | ||
System.err.println(line); | ||
System.err.println(nextLine); | ||
while ((nextLine = reader.readLine()) != null) { | ||
System.err.println(nextLine); | ||
} | ||
} | ||
return line; | ||
} | ||
finally { | ||
con.disconnect(); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -193,6 +193,7 @@ public static class RestrictedApi extends Api { | |
} | ||
|
||
@Override public void doXml(StaplerRequest req, StaplerResponse rsp, @QueryParameter String xpath, @QueryParameter String wrapper, @QueryParameter String tree, @QueryParameter int depth) throws IOException, ServletException { | ||
setHeaders(rsp); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes the crumb code work. No longer necessary for this PR, which deletes the code that called this endpoint from the context of the CLI (would make that code in old clients start working, which is fine but unnecessary); but generally seems wise to keep the behavior of |
||
String text; | ||
CrumbIssuer ci = (CrumbIssuer) bean; | ||
if ("/*/crumbRequestField/text()".equals(xpath)) { // old FullDuplexHttpStream | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,5 @@ | ||
package hudson.cli; | ||
|
||
import com.gargoylesoftware.htmlunit.HttpMethod; | ||
import com.gargoylesoftware.htmlunit.Page; | ||
import com.gargoylesoftware.htmlunit.WebRequest; | ||
import com.gargoylesoftware.htmlunit.WebResponse; | ||
import com.google.common.collect.Lists; | ||
import hudson.Functions; | ||
import hudson.Launcher; | ||
|
@@ -26,7 +22,6 @@ | |
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.UUID; | ||
import java.util.concurrent.ExecutorService; | ||
import java.util.concurrent.Executors; | ||
import java.util.concurrent.TimeUnit; | ||
|
@@ -122,16 +117,6 @@ public void serveCliActionToAnonymousUserWithoutPermissions() throws Exception { | |
JenkinsRule.WebClient wc = j.createWebClient(); | ||
// The behavior changed due to SECURITY-192. index page is no longer accessible to anonymous | ||
wc.assertFails("cli", HttpURLConnection.HTTP_FORBIDDEN); | ||
// so we check the access by emulating the CLI connection post request | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually would pass again given the restored crumb exclusion, but is anyway redundant given more comprehensive, and realistic, tests later added to the CLI system. |
||
WebRequest settings = new WebRequest(new URL(j.getURL(), "cli")); | ||
settings.setHttpMethod(HttpMethod.POST); | ||
settings.setAdditionalHeader("Session", UUID.randomUUID().toString()); | ||
settings.setAdditionalHeader("Side", "download"); // We try to download something to init the duplex channel | ||
|
||
Page page = wc.getPage(settings); | ||
WebResponse webResponse = page.getWebResponse(); | ||
assertEquals("We expect that the proper POST request from CLI gets processed successfully", | ||
200, webResponse.getStatusCode()); | ||
} | ||
|
||
@Test | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
/* | ||
* The MIT License | ||
* | ||
* Copyright 2017 CloudBees, Inc. | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy | ||
* of this software and associated documentation files (the "Software"), to deal | ||
* in the Software without restriction, including without limitation the rights | ||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
* copies of the Software, and to permit persons to whom the Software is | ||
* furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in | ||
* all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
* THE SOFTWARE. | ||
*/ | ||
package jenkins.util; | ||
|
||
import hudson.cli.FullDuplexHttpStream; | ||
import hudson.model.RootAction; | ||
import hudson.security.csrf.CrumbExclusion; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.io.OutputStream; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.UUID; | ||
import java.util.logging.Level; | ||
import java.util.logging.Logger; | ||
import javax.servlet.FilterChain; | ||
import javax.servlet.ServletException; | ||
import javax.servlet.http.HttpServletRequest; | ||
import javax.servlet.http.HttpServletResponse; | ||
import org.junit.Test; | ||
import static org.junit.Assert.*; | ||
import org.junit.Rule; | ||
import org.jvnet.hudson.test.JenkinsRule; | ||
import org.jvnet.hudson.test.LoggerRule; | ||
import org.jvnet.hudson.test.TestExtension; | ||
import org.kohsuke.stapler.HttpResponse; | ||
import org.kohsuke.stapler.StaplerRequest; | ||
|
||
public class FullDuplexHttpServiceTest { | ||
|
||
@Rule | ||
public JenkinsRule r = new JenkinsRule(); | ||
|
||
@Rule | ||
public LoggerRule logging = new LoggerRule().record(FullDuplexHttpService.class, Level.FINE).record(FullDuplexHttpStream.class, Level.FINE); | ||
|
||
@Test | ||
public void smokes() throws Exception { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just for fun! |
||
logging.record("org.eclipse.jetty", Level.ALL); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have been investigating failures of HTTP Duplex to work in test cases using Jetty 9.4.x. Symptoms are typically a failure to connect the upload side, as in JENKINS-43666. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Starting from #2959 which picks up jenkinsci/jenkins-test-harness#63. Seems unrelated to #3011.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How verbose is this? Intended to stay on ALL? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In practice Jetty logs everything at |
||
FullDuplexHttpStream con = new FullDuplexHttpStream(r.getURL(), "test/", null); | ||
InputStream is = con.getInputStream(); | ||
OutputStream os = con.getOutputStream(); | ||
os.write(33); | ||
os.flush(); | ||
Logger.getLogger(FullDuplexHttpServiceTest.class.getName()).info("uploaded initial content"); | ||
assertEquals(0, is.read()); // see FullDuplexHttpStream.getInputStream | ||
assertEquals(66, is.read()); | ||
} | ||
@TestExtension("smokes") | ||
public static class Endpoint implements RootAction { | ||
private transient final Map<UUID, FullDuplexHttpService> duplexServices = new HashMap<>(); | ||
@Override | ||
public String getUrlName() { | ||
return "test"; | ||
} | ||
@Override | ||
public String getIconFileName() { | ||
return null; | ||
} | ||
@Override | ||
public String getDisplayName() { | ||
return null; | ||
} | ||
public HttpResponse doIndex() { | ||
return new FullDuplexHttpService.Response(duplexServices) { | ||
@Override | ||
protected FullDuplexHttpService createService(StaplerRequest req, UUID uuid) throws IOException, InterruptedException { | ||
return new FullDuplexHttpService(uuid) { | ||
@Override | ||
protected void run(InputStream upload, OutputStream download) throws IOException, InterruptedException { | ||
int x = upload.read(); | ||
download.write(x * 2); | ||
} | ||
}; | ||
} | ||
}; | ||
} | ||
} | ||
@TestExtension("smokes") | ||
public static class EndpointCrumbExclusion extends CrumbExclusion { | ||
@Override | ||
public boolean process(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { | ||
if ("/test/".equals(request.getPathInfo())) { | ||
chain.doFilter(request, response); | ||
return true; | ||
} | ||
return false; | ||
} | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally useful.