Skip to content

Commit

Permalink
[JENKINS-70240] Support non-HTTP URLs in `PluginManager#doCheckUpdate…
Browse files Browse the repository at this point in the history
…SiteUrl` (#7524)

Co-authored-by: Julie Heard <55280278+julieheard@users.noreply.github.com>
Co-authored-by: Basil Crow <me@basilcrow.com>
  • Loading branch information
3 people committed Dec 17, 2022
1 parent fd5237c commit 0ddfd46
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 29 deletions.
79 changes: 53 additions & 26 deletions core/src/main/java/hudson/PluginManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -1960,44 +1960,71 @@ public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, Servl
@Restricted(NoExternalUse.class)
@RequirePOST public FormValidation doCheckUpdateSiteUrl(StaplerRequest request, @QueryParameter String value) throws InterruptedException {
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
return checkUpdateSiteURL(value);
}

@Restricted(DoNotUse.class) // visible for testing only
FormValidation checkUpdateSiteURL(@CheckForNull String value) throws InterruptedException {
value = Util.fixEmptyAndTrim(value);

if (value == null) {
return FormValidation.error(Messages.PluginManager_emptyUpdateSiteUrl());
}

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;
final URI baseUri;
try {
httpRequest = ProxyConfiguration.newHttpRequestBuilder(uri)
.method("HEAD", HttpRequest.BodyPublishers.noBody())
.build();
} catch (IllegalArgumentException e) {
return FormValidation.error(e, Messages.PluginManager_invalidUrl());
baseUri = new URI(value);
} catch (URISyntaxException ex) {
return FormValidation.error(ex, 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) {

if ("file".equalsIgnoreCase(baseUri.getScheme())) {
File f = new File(baseUri);
if (f.isFile()) {
return FormValidation.ok();
}
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());
}

if ("https".equalsIgnoreCase(baseUri.getScheme()) || "http".equalsIgnoreCase(baseUri.getScheme())) {
final URI uriWithQuery;
try {
if (baseUri.getRawQuery() == null) {
uriWithQuery = new URI(value + "?version=" + Jenkins.VERSION + "&uctest");
} else {
uriWithQuery = new URI(value + "&version=" + Jenkins.VERSION + "&uctest");
}
} 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(uriWithQuery)
.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();
}
LOGGER.log(Level.FINE, "Obtained a non OK ({0}) response from the update center",
new Object[] {httpResponse.statusCode(), baseUri});
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());
}

}
// not a file or http(s) scheme
return FormValidation.error(Messages.PluginManager_invalidUrl());
}

@Restricted(NoExternalUse.class)
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/resources/hudson/Messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ PluginManager.deprecationWarning=<strong>This plugin is deprecated.</strong> In
PluginManager.insecureUrl=\
You are using an insecure URL to download the plugin, use at your own risk!
PluginManager.invalidUrl=\
You are using an invalid URL to download the plugin, only https and http (not recommended) are supported.
You are using an invalid URL to download the plugin. file, https and http (not recommended) URLs are supported.

PluginManager.emptyUpdateSiteUrl=\
The update site cannot be empty. Please enter a valid url.
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/resources/hudson/Messages_pt_BR.properties
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ FilePath.validateAntFileMask.doesntMatchAnythingAndSuggest="{0}" não retornou n
PluginWrapper.NoSuchPlugin=Nenhuma extensão encontrada com o nome ''{0}''
PluginWrapper.failed_to_load_plugin_2=Falhou para carregar: {0} ({1} {2})
PluginManager.invalidUrl= \
Você está usando uma URL inválida para baixar a extensão, somente https e http (não recomendado) são suportados.
Você está usando uma URL inválida para baixar a extensão, somente file, https e http (não recomendado) são suportados.
PluginWrapper.Already.Disabled=A extensão ''{0}'' já foi desabilitada
PluginWrapper.disabled_2=A extensão requerida está desabilitada: {0} {1}
PluginManager.parentDepCompatWarning=As seguintes extensões são incompatíveis:
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/resources/hudson/Messages_zh_TW.properties
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ PluginManager.newerVersionEntry=已有此版本的外掛但不作為更新提供
PluginManager.unavailable=無法使用
PluginManager.deprecationWarning=<strong>此外掛已棄用。</strong>這通常表示它可能已過時、不再繼續開發、不再正常運作等。<a href\="{0}" rel\="noopener noreferrer" target\="_blank">了解更多。</a>
PluginManager.insecureUrl=您正在使用不安全的 URL 下載該外掛,風險請自負!
PluginManager.invalidUrl=您正在使用無效的 URL 下載該外掛,只支援 httpshttp (不建議)。
PluginManager.invalidUrl=您正在使用無效的 URL 下載該外掛,只支援 httpshttp (不建議) 和 file


AboutJenkins.DisplayName=關於 Jenkins
Expand Down
65 changes: 65 additions & 0 deletions core/src/test/java/hudson/PluginManagerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import hudson.util.FormValidation;
import hudson.util.FormValidation.Kind;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
Expand All @@ -44,6 +48,8 @@
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.jvnet.hudson.test.Issue;
Expand Down Expand Up @@ -114,6 +120,40 @@ public void shouldProperlyRetrieveModificationDate() throws IOException {
equalTo(jar.lastModified()));
}


@Test
@Issue("JENKINS-70420")
public void updateSiteURLCheckValidation() throws Exception {
LocalPluginManager pm = new LocalPluginManager(tmp.toFile());

assertThat("ftp urls are not acceptable", pm.checkUpdateSiteURL("ftp://foo/bar"),
allOf(FormValidationMatcher.validationWithMessage(Kind.ERROR), hasProperty("message", containsString("invalid URL"))));
assertThat("file urls to non files are not acceptable", pm.checkUpdateSiteURL(tmp.toUri().toURL().toString()),
allOf(FormValidationMatcher.validationWithMessage(Kind.ERROR), hasProperty("message", containsString("Unable to connect to the URL"))));

assertThat("invalid URLs do not cause a stack tracek", pm.checkUpdateSiteURL("sufslef3,r3;r99 3 l4i34"),
allOf(FormValidationMatcher.validationWithMessage(Kind.ERROR), hasProperty("message", containsString("invalid URL"))));

assertThat("empty url message", pm.checkUpdateSiteURL(""),
allOf(FormValidationMatcher.validationWithMessage(Kind.ERROR), hasProperty("message", containsString("cannot be empty"))));
assertThat("null url message", pm.checkUpdateSiteURL(""),
allOf(FormValidationMatcher.validationWithMessage(Kind.ERROR), hasProperty("message", containsString("cannot be empty"))));

// create a tempoary local file
Path p = tmp.resolve("some.json");
Files.writeString(tmp.resolve("some.json"), "{}");
assertThat("file urls pointing to existing files work", pm.checkUpdateSiteURL(p.toUri().toURL().toString()),
FormValidationMatcher.validationWithMessage(Kind.OK));

assertThat("http urls with non existing servers", pm.checkUpdateSiteURL("https://bogus.example.com"),
allOf(FormValidationMatcher.validationWithMessage(Kind.ERROR), hasProperty("message", containsString("Unable to connect to the URL"))));

// starting a http server here is likely to be overkill and given this is the predominant use case is not so likely to regress.
assertThat("main UC validates correctly", pm.checkUpdateSiteURL("https://updates.jenkins.io/update-center.json"),
FormValidationMatcher.validationWithMessage(Kind.OK));

}

private static void assertAttribute(Manifest manifest, String attributeName, String value) {
Attributes attributes = manifest.getMainAttributes();
assertThat("Main attributes must not be empty", attributes, notNullValue());
Expand Down Expand Up @@ -168,4 +208,29 @@ private URL toManifestUrl(File jarFile) throws MalformedURLException {
final String manifestPath = "META-INF/MANIFEST.MF";
return new URL("jar:" + jarFile.toURI().toURL() + "!/" + manifestPath);
}

private static class FormValidationMatcher extends TypeSafeDiagnosingMatcher<FormValidation> {

private final Kind kind;

private FormValidationMatcher(Kind kind) {
this.kind = kind;
}

@Override
public void describeTo(Description description) {
description.appendText("FormValidation of type ").appendValue(kind);
}

@Override
protected boolean matchesSafely(FormValidation item, Description mismatchDescription) {
mismatchDescription.appendText("FormValidation of type ").appendValue(item.kind);
return item.kind == kind;
}

static FormValidationMatcher validationWithMessage(Kind kind) {
return new FormValidationMatcher(kind);
}

}
}

0 comments on commit 0ddfd46

Please sign in to comment.