diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 0037474700..5bc21ee782 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -1960,44 +1960,71 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas @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"; + final URI baseUri; + try { + baseUri = new URI(value); + } catch (URISyntaxException ex) { + return FormValidation.error(ex, Messages.PluginManager_invalidUrl()); + } - 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 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 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) diff --git a/core/src/main/resources/hudson/Messages.properties b/core/src/main/resources/hudson/Messages.properties index 8e6a0d3836..afd093dfa0 100644 --- a/core/src/main/resources/hudson/Messages.properties +++ b/core/src/main/resources/hudson/Messages.properties @@ -100,7 +100,7 @@ PluginManager.deprecationWarning=This plugin is deprecated. 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. diff --git a/core/src/main/resources/hudson/Messages_pt_BR.properties b/core/src/main/resources/hudson/Messages_pt_BR.properties index b368f78679..e1b6550207 100644 --- a/core/src/main/resources/hudson/Messages_pt_BR.properties +++ b/core/src/main/resources/hudson/Messages_pt_BR.properties @@ -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: diff --git a/core/src/main/resources/hudson/Messages_zh_TW.properties b/core/src/main/resources/hudson/Messages_zh_TW.properties index bf7f1fe6c6..12d7317ff5 100644 --- a/core/src/main/resources/hudson/Messages_zh_TW.properties +++ b/core/src/main/resources/hudson/Messages_zh_TW.properties @@ -74,7 +74,7 @@ PluginManager.newerVersionEntry=已有此版本的外掛但不作為更新提供 PluginManager.unavailable=無法使用 PluginManager.deprecationWarning=此外掛已棄用。這通常表示它可能已過時、不再繼續開發、不再正常運作等。了解更多。 PluginManager.insecureUrl=您正在使用不安全的 URL 下載該外掛,風險請自負! -PluginManager.invalidUrl=您正在使用無效的 URL 下載該外掛,只支援 https 和 http (不建議)。 +PluginManager.invalidUrl=您正在使用無效的 URL 下載該外掛,只支援 https、http (不建議) 和 file。 AboutJenkins.DisplayName=關於 Jenkins diff --git a/core/src/test/java/hudson/PluginManagerTest.java b/core/src/test/java/hudson/PluginManagerTest.java index 87e074477c..46d27f49ea 100644 --- a/core/src/test/java/hudson/PluginManagerTest.java +++ b/core/src/test/java/hudson/PluginManagerTest.java @@ -27,11 +27,15 @@ package hudson; 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; @@ -44,6 +48,8 @@ import java.util.jar.Attributes; 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; @@ -114,6 +120,40 @@ public class PluginManagerTest { 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()); @@ -168,4 +208,29 @@ public class PluginManagerTest { final String manifestPath = "META-INF/MANIFEST.MF"; return new URL("jar:" + jarFile.toURI().toURL() + "!/" + manifestPath); } + + private static class FormValidationMatcher extends TypeSafeDiagnosingMatcher { + + 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); + } + + } }