Skip to content
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

Introduce a high-level HTTP client API #7398

Merged
merged 5 commits into from
Nov 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 39 additions & 22 deletions core/src/main/java/hudson/PluginManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,21 @@
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.security.CodeSource;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -1956,31 +1959,45 @@ public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, Servl
}

@Restricted(NoExternalUse.class)
@RequirePOST public FormValidation doCheckUpdateSiteUrl(StaplerRequest request, @QueryParameter String value) {
@RequirePOST public FormValidation doCheckUpdateSiteUrl(StaplerRequest request, @QueryParameter String value) throws InterruptedException {
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
if (StringUtils.isNotBlank(value)) {
try {
value += ((value.contains("?")) ? "&" : "?") + "version=" + Jenkins.VERSION + "&uctest";

URL url = new URL(value);
value = Util.fixEmptyAndTrim(value);
if (value == null) {
return FormValidation.error(Messages.PluginManager_emptyUpdateSiteUrl());
}

// Connect to the URL
HttpURLConnection conn = (HttpURLConnection) ProxyConfiguration.open(url);
conn.setRequestMethod("HEAD");
conn.setConnectTimeout(5000);
if (100 <= conn.getResponseCode() && conn.getResponseCode() <= 399) {
return FormValidation.ok();
} else {
LOGGER.log(Level.FINE, "Obtained a non OK ({0}) response from the update center",
new Object[]{conn.getResponseCode(), url});
return FormValidation.error(Messages.PluginManager_connectionFailed());
}
} catch (IOException e) {
LOGGER.log(Level.FINE, "Failed to check update site", e);
return FormValidation.error(Messages.PluginManager_connectionFailed());
value += ((value.contains("?")) ? "&" : "?") + "version=" + Jenkins.VERSION + "&uctest";

URI uri;
try {
uri = new URI(value);
} catch (URISyntaxException e) {
return FormValidation.error(e, Messages.PluginManager_invalidUrl());
}
HttpClient httpClient = ProxyConfiguration.newHttpClientBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
HttpRequest httpRequest;
try {
httpRequest = ProxyConfiguration.newHttpRequestBuilder(uri)
.method("HEAD", HttpRequest.BodyPublishers.noBody())
.build();
} catch (IllegalArgumentException e) {
return FormValidation.error(e, Messages.PluginManager_invalidUrl());
}
try {
java.net.http.HttpResponse<Void> httpResponse = httpClient.send(
httpRequest, java.net.http.HttpResponse.BodyHandlers.discarding());
if (100 <= httpResponse.statusCode() && httpResponse.statusCode() <= 399) {
return FormValidation.ok();
}
} else {
return FormValidation.error(Messages.PluginManager_emptyUpdateSiteUrl());
LOGGER.log(Level.FINE, "Obtained a non OK ({0}) response from the update center",
new Object[] {httpResponse.statusCode(), uri});
return FormValidation.error(Messages.PluginManager_connectionFailed());
} catch (IOException e) {
LOGGER.log(Level.FINE, "Failed to check update site", e);
return FormValidation.error(e, Messages.PluginManager_connectionFailed());
}
}

Expand Down
165 changes: 135 additions & 30 deletions core/src/main/java/hudson/ProxyConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
Expand All @@ -56,8 +57,10 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import jenkins.UserAgentURLConnectionDecorator;
import jenkins.model.Jenkins;
import jenkins.security.stapler.StaplerAccessibleType;
import jenkins.util.JenkinsJVM;
Expand Down Expand Up @@ -225,6 +228,10 @@ public static List<Pattern> getNoProxyHostPatterns(String noProxyHost) {
return r;
}

private static boolean isExcluded(String needle, String haystack) {
return getNoProxyHostPatterns(haystack).stream().anyMatch(p -> p.matcher(needle).matches());
}

@DataBoundSetter
public void setSecretPassword(Secret secretPassword) {
this.secretPassword = secretPassword;
Expand Down Expand Up @@ -259,11 +266,8 @@ public Proxy createProxy(String host) {
}

public static Proxy createProxy(String host, String name, int port, String noProxyHost) {
if (host != null && noProxyHost != null) {
for (Pattern p : getNoProxyHostPatterns(noProxyHost)) {
if (p.matcher(host).matches())
return Proxy.NO_PROXY;
}
if (host != null && noProxyHost != null && isExcluded(host, noProxyHost)) {
return Proxy.NO_PROXY;
}
return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(name, port));
}
Expand Down Expand Up @@ -299,7 +303,10 @@ public static ProxyConfiguration load() throws IOException {

/**
* This method should be used wherever {@link URL#openConnection()} to internet URLs is invoked directly.
*
* @deprecated use {@link #newHttpClient}/{@link #newHttpClientBuilder} and {@link #newHttpRequestBuilder(URI)}
*/
@Deprecated
public static URLConnection open(URL url) throws IOException {
final ProxyConfiguration p = get();

Expand Down Expand Up @@ -327,6 +334,10 @@ public static URLConnection open(URL url) throws IOException {
return con;
}

/**
* @deprecated use {@link #newHttpClient}/{@link #newHttpClientBuilder} and {@link #newHttpRequestBuilder(URI)}
*/
@Deprecated
public static InputStream getInputStream(URL url) throws IOException {
final ProxyConfiguration p = get();
if (p == null)
Expand All @@ -343,6 +354,100 @@ public static InputStream getInputStream(URL url) throws IOException {
return is;
}

/**
* Return a new {@link HttpClient} with Jenkins-specific default settings.
*
* <p>Equivalent to {@code newHttpClientBuilder().build()}.
*
* <p>The Jenkins-specific default settings include a proxy server and proxy authentication (as
* configured by {@link ProxyConfiguration}) and a connection timeout (as configured by {@link
* ProxyConfiguration#DEFAULT_CONNECT_TIMEOUT_MILLIS}).
*
* @return a new {@link HttpClient}
* @since TODO
*/
public static HttpClient newHttpClient() {
return newHttpClientBuilder().build();
}

/**
* Create a new {@link HttpClient.Builder} preconfigured with Jenkins-specific default settings.
*
* <p>The Jenkins-specific default settings include a proxy server and proxy authentication (as
* configured by {@link ProxyConfiguration}) and a connection timeout (as configured by {@link
* ProxyConfiguration#DEFAULT_CONNECT_TIMEOUT_MILLIS}).
*
* @return an {@link HttpClient.Builder}
* @since TODO
*/
public static HttpClient.Builder newHttpClientBuilder() {
HttpClient.Builder httpClientBuilder = HttpClient.newBuilder();
ProxyConfiguration proxyConfiguration = get();
if (proxyConfiguration != null) {
if (proxyConfiguration.getName() != null) {
httpClientBuilder.proxy(new JenkinsProxySelector(
proxyConfiguration.getName(),
proxyConfiguration.getPort(),
proxyConfiguration.getNoProxyHost()));
}
if (proxyConfiguration.getUserName() != null) {
httpClientBuilder.authenticator(proxyConfiguration.authenticator);
}
}
if (DEFAULT_CONNECT_TIMEOUT_MILLIS > 0) {
httpClientBuilder.connectTimeout(Duration.ofMillis(DEFAULT_CONNECT_TIMEOUT_MILLIS));
}
return httpClientBuilder;
}

/**
* Create a new {@link HttpRequest.Builder} builder with the given URI preconfigured with
* Jenkins-specific default settings.
*
* <p>The Jenkins-specific default settings include a custom user agent on the controller
* (unless {@link UserAgentURLConnectionDecorator#DISABLED} is true).
*
* @param uri the request URI
* @return an {@link HttpRequest.Builder}
* @throws IllegalArgumentException if the URI scheme is not supported
* @since TODO
*/
public static HttpRequest.Builder newHttpRequestBuilder(URI uri) {
HttpRequest.Builder httpRequestBuilder = HttpRequest.newBuilder(uri);
if (JenkinsJVM.isJenkinsJVM() && !UserAgentURLConnectionDecorator.DISABLED) {
httpRequestBuilder.setHeader("User-Agent", UserAgentURLConnectionDecorator.getUserAgent());
}
return httpRequestBuilder;
}

private static class JenkinsProxySelector extends ProxySelector {
@NonNull private final Proxy proxy;
@CheckForNull private final String exclusions;

private JenkinsProxySelector(@NonNull String hostname, int port, @CheckForNull String exclusions) {
this.proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(hostname, port));
this.exclusions = exclusions;
}

@Override
public void connectFailed(URI uri, SocketAddress sa, IOException e) {
// Ignore.
}

@Override
public List<Proxy> select(URI uri) {
Objects.requireNonNull(uri);
String scheme = Objects.requireNonNull(uri.getScheme());
String host = Objects.requireNonNull(uri.getHost());
boolean excluded = exclusions != null && isExcluded(host.toLowerCase(), exclusions);
if (!excluded && (scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))) {
return List.of(proxy);
} else {
return List.of(Proxy.NO_PROXY);
}
}
}

/**
* If the first URL we try to access with a HTTP proxy is HTTPS then the authentication cache will not have been
* pre-populated, so we try to access at least one HTTP URL before the very first HTTPS url.
Expand Down Expand Up @@ -433,44 +538,44 @@ public FormValidation doValidateProxy(

Jenkins.get().checkPermission(Jenkins.ADMINISTER);

if (Util.fixEmptyAndTrim(testUrl) == null) {
testUrl = Util.fixEmptyAndTrim(testUrl);
if (testUrl == null) {
return FormValidation.error(Messages.ProxyConfiguration_TestUrlRequired());
}

URI uri;
try {
uri = new URI(testUrl);
} catch (URISyntaxException e) {
return FormValidation.error(Messages.ProxyConfiguration_MalformedTestUrl(testUrl));
return FormValidation.error(e, Messages.ProxyConfiguration_MalformedTestUrl(testUrl));
}

HttpClient.Builder builder = HttpClient.newBuilder();
builder.connectTimeout(DEFAULT_CONNECT_TIMEOUT_MILLIS > 0
? Duration.ofMillis(DEFAULT_CONNECT_TIMEOUT_MILLIS)
: Duration.ofSeconds(30));
if (Util.fixEmptyAndTrim(name) != null && !isNoProxyHost(uri.getHost(), noProxyHost)) {
builder.proxy(ProxySelector.of(new InetSocketAddress(name, port)));
Authenticator authenticator = newValidationAuthenticator(userName, password != null ? password.getPlainText() : null);
builder.authenticator(authenticator);
}
HttpClient httpClient = builder.build();
HttpRequest httpRequest;
try {
HttpClient.Builder builder = HttpClient.newBuilder();
builder.connectTimeout(
DEFAULT_CONNECT_TIMEOUT_MILLIS > 0
? Duration.ofMillis(DEFAULT_CONNECT_TIMEOUT_MILLIS)
: Duration.ofSeconds(30));
if (Util.fixEmptyAndTrim(name) != null && !isNoProxyHost(uri.getHost(), noProxyHost)) {
builder.proxy(ProxySelector.of(new InetSocketAddress(name, port)));
Authenticator authenticator =
newValidationAuthenticator(
userName, password != null ? password.getPlainText() : null);
builder.authenticator(authenticator);
}
HttpClient client = builder.build();

HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
HttpResponse<Void> response =
client.send(request, HttpResponse.BodyHandlers.discarding());
int code = response.statusCode();
if (code != HttpURLConnection.HTTP_OK) {
return FormValidation.error(Messages.ProxyConfiguration_FailedToConnect(testUrl, code));
httpRequest = ProxyConfiguration.newHttpRequestBuilder(uri)
.method("HEAD", HttpRequest.BodyPublishers.noBody())
.build();
} catch (IllegalArgumentException e) {
return FormValidation.error(e, Messages.ProxyConfiguration_MalformedTestUrl(testUrl));
}
try {
HttpResponse<Void> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.discarding());
if (httpResponse.statusCode() == HttpURLConnection.HTTP_OK) {
return FormValidation.ok(Messages.ProxyConfiguration_Success());
}
return FormValidation.error(Messages.ProxyConfiguration_FailedToConnect(testUrl, httpResponse.statusCode()));
} catch (IOException e) {
return FormValidation.error(e, Messages.ProxyConfiguration_FailedToConnectViaProxy(testUrl));
}

return FormValidation.ok(Messages.ProxyConfiguration_Success());
}

private boolean isNoProxyHost(String host, String noProxyHost) {
Expand Down
46 changes: 32 additions & 14 deletions core/src/main/java/hudson/tools/ZipExtractionInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import jenkins.MasterToSlaveFileCallable;
import jenkins.model.Jenkins;
import org.jenkinsci.Symbol;
Expand Down Expand Up @@ -100,22 +103,37 @@ public String getDisplayName() {
}

@RequirePOST
public FormValidation doCheckUrl(@QueryParameter String value) {
public FormValidation doCheckUrl(@QueryParameter String value) throws InterruptedException {
Jenkins.get().checkPermission(Jenkins.ADMINISTER);

value = Util.fixEmptyAndTrim(value);
if (value == null) {
return FormValidation.ok();
}

URI uri;
try {
URLConnection conn = ProxyConfiguration.open(new URL(value));
conn.connect();
if (conn instanceof HttpURLConnection) {
Copy link
Member Author

@basil basil Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caused JENKINS-75003.

if (((HttpURLConnection) conn).getResponseCode() != HttpURLConnection.HTTP_OK) {
return FormValidation.error(Messages.ZipExtractionInstaller_bad_connection());
}
uri = new URI(value);
} catch (URISyntaxException e) {
return FormValidation.error(e, Messages.ZipExtractionInstaller_malformed_url());
}
HttpClient httpClient = ProxyConfiguration.newHttpClient();
HttpRequest httpRequest;
try {
httpRequest = ProxyConfiguration.newHttpRequestBuilder(uri)
.method("HEAD", HttpRequest.BodyPublishers.noBody())
.build();
} catch (IllegalArgumentException e) {
return FormValidation.error(e, Messages.ZipExtractionInstaller_malformed_url());
}
try {
HttpResponse<Void> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.discarding());
if (httpResponse.statusCode() == HttpURLConnection.HTTP_OK) {
return FormValidation.ok();
}
return FormValidation.ok();
} catch (MalformedURLException x) {
return FormValidation.error(Messages.ZipExtractionInstaller_malformed_url());
} catch (IOException x) {
return FormValidation.error(x, Messages.ZipExtractionInstaller_could_not_connect());
return FormValidation.error(Messages.ZipExtractionInstaller_bad_connection());
} catch (IOException e) {
return FormValidation.error(e, Messages.ZipExtractionInstaller_could_not_connect());
}
}

Expand Down
Loading