diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3d64ea5..0049e5e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,4 +3,8 @@ updates: - package-ecosystem: "maven" directory: "/" schedule: - interval: "weekly" \ No newline at end of file + interval: "weekly" + - package-ecosystem: "github-actions" + directory: / + schedule: + interval: "daily" \ No newline at end of file diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 7248cbc..27c5566 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,4 +1,2 @@ _extends: .github -name-template: v$NEXT_MINOR_VERSION 🌈 -tag-template: sshd-$NEXT_MINOR_VERSION diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml new file mode 100644 index 0000000..0279984 --- /dev/null +++ b/.github/workflows/cd.yaml @@ -0,0 +1,15 @@ +# Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins + +name: cd +on: + workflow_dispatch: + check_run: + types: + - completed + +jobs: + maven-cd: + uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@v1 + secrets: + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml deleted file mode 100644 index bf660a2..0000000 --- a/.github/workflows/changelog.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Release Drafter - -on: - push: - # branches to consider in the event; optional, defaults to all - branches: - - master - -jobs: - update_release_draft: - runs-on: ubuntu-latest - steps: - # Drafts your next Release notes as Pull Requests are merged into "master" - - uses: release-drafter/release-drafter@v5 - with: - publish: ${{ contains(github.event.head_commit.message, '[maven-release-plugin] prepare release') }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - diff --git a/.mvn/maven.config b/.mvn/maven.config index 2a0299c..f7daf60 100644 --- a/.mvn/maven.config +++ b/.mvn/maven.config @@ -1,2 +1,3 @@ -Pconsume-incrementals -Pmight-produce-incrementals +-Dchangelist.format=%d.v%s diff --git a/pom.xml b/pom.xml index 0d94cdc..c77fc74 100644 --- a/pom.xml +++ b/pom.xml @@ -3,13 +3,13 @@ org.jenkins-ci.plugins plugin - 4.37 + 4.41 org.jenkins-ci.modules sshd - ${revision}${changelist} + ${revision}.${changelist} hpi SSH server Adds SSH server functionality to Jenkins, exposing CLI commands through it @@ -29,29 +29,16 @@ - 3.1.1 - -SNAPSHOT + 3 + 999999-SNAPSHOT 2.289.1 - 8 - - - - io.jenkins.tools.bom - bom-2.277.x - 961.vf0c9f6f59827 - pom - import - - - - - org.apache.sshd - sshd-core - 2.7.0 + io.jenkins.plugins.mina-sshd-api + mina-sshd-api-core + 2.8.0-18.vd98674ecd652 net.i2p.crypto @@ -61,7 +48,7 @@ org.jenkins-ci.modules instance-identity - 2.2 + provided org.jenkins-ci diff --git a/src/main/java/org/jenkinsci/main/modules/sshd/AsynchronousCommand.java b/src/main/java/org/jenkinsci/main/modules/sshd/AsynchronousCommand.java index a6debb3..efe3b01 100644 --- a/src/main/java/org/jenkinsci/main/modules/sshd/AsynchronousCommand.java +++ b/src/main/java/org/jenkinsci/main/modules/sshd/AsynchronousCommand.java @@ -81,9 +81,9 @@ public void setSession(ServerSession session) { @CheckForNull protected User getCurrentUser() { - final Jenkins jenkins = Jenkins.getInstance(); - if (jenkins != null && jenkins.isUseSecurity()) { - return User.get(getSession().getUsername()); // then UserAuthNamedFactory must have done public key auth + final Jenkins jenkins = Jenkins.get(); + if (jenkins.isUseSecurity()) { + return User.getById(getSession().getUsername(), true); // then UserAuthNamedFactory must have done public key auth } else { return null; // not authenticated. anonymous. } diff --git a/src/main/java/org/jenkinsci/main/modules/sshd/CLICommandAdapter.java b/src/main/java/org/jenkinsci/main/modules/sshd/CLICommandAdapter.java index cd88324..3c72c77 100644 --- a/src/main/java/org/jenkinsci/main/modules/sshd/CLICommandAdapter.java +++ b/src/main/java/org/jenkinsci/main/modules/sshd/CLICommandAdapter.java @@ -1,13 +1,15 @@ package org.jenkinsci.main.modules.sshd; +import hudson.CloseProofOutputStream; import hudson.Extension; import hudson.cli.CLICommand; import hudson.model.User; +import org.apache.sshd.server.command.Command; + import java.io.IOException; import java.io.PrintStream; import java.nio.charset.Charset; import java.util.Locale; -import org.apache.sshd.server.command.Command; /** * {@link SshCommandFactory} that invokes {@link CLICommand}s. @@ -26,14 +28,17 @@ public Command create(CommandLine commandLine) { @Override public int runCommand() throws IOException { User u = getCurrentUser(); - if (u!=null) c.setTransportAuth(u.impersonate()); + if (u != null) { + c.setTransportAuth2(u.impersonate2()); + } CommandLine cmds = getCmdLine(); //TODO: Consider switching to UTF-8 + //TODO: Consider removing the CloseProofOutputStream wrapper when SSHD-1257 is available return c.main(cmds.subList(1,cmds.size()), Locale.getDefault(), getInputStream(), - new PrintStream(getOutputStream(), false, Charset.defaultCharset().toString()), - new PrintStream(getErrorStream(), false, Charset.defaultCharset().toString())); + new PrintStream(new CloseProofOutputStream(getOutputStream()), false, Charset.defaultCharset().toString()), + new PrintStream(new CloseProofOutputStream(getErrorStream()), false, Charset.defaultCharset().toString())); } }; } diff --git a/src/main/java/org/jenkinsci/main/modules/sshd/PortAdvertiser.java b/src/main/java/org/jenkinsci/main/modules/sshd/PortAdvertiser.java index 7df8afd..851d905 100644 --- a/src/main/java/org/jenkinsci/main/modules/sshd/PortAdvertiser.java +++ b/src/main/java/org/jenkinsci/main/modules/sshd/PortAdvertiser.java @@ -25,10 +25,7 @@ public String getEndpoint() { try { int p = sshd.getActualPort(); if (p>0) { - final Jenkins jenkins = Jenkins.getInstance(); - if (jenkins == null) { - throw new IllegalStateException("Jenkins has not been started, or was already shut down"); - } + final Jenkins jenkins = Jenkins.get(); return (host != null ? host : new URL(jenkins.getRootUrl()).getHost()) + ":" + p; } } catch (Exception e) { diff --git a/src/main/java/org/jenkinsci/main/modules/sshd/UserAuthNamedFactory.java b/src/main/java/org/jenkinsci/main/modules/sshd/UserAuthNamedFactory.java index 29ee8da..990f5bd 100644 --- a/src/main/java/org/jenkinsci/main/modules/sshd/UserAuthNamedFactory.java +++ b/src/main/java/org/jenkinsci/main/modules/sshd/UserAuthNamedFactory.java @@ -18,8 +18,8 @@ class UserAuthNamedFactory implements UserAuthFactory { UserAuthFactory none = UserAuthNoneFactory.INSTANCE; private UserAuthFactory select() { - final Jenkins jenkins = Jenkins.getInstance(); - return (jenkins != null && jenkins.isUseSecurity()) ? publicKey : none; + final Jenkins jenkins = Jenkins.get(); + return jenkins.isUseSecurity() ? publicKey : none; } public String getName() { diff --git a/src/main/resources/index.jelly b/src/main/resources/index.jelly new file mode 100644 index 0000000..496cc22 --- /dev/null +++ b/src/main/resources/index.jelly @@ -0,0 +1,2 @@ + +
Adds SSH server functionality to Jenkins, exposing CLI commands through it.
\ No newline at end of file diff --git a/src/test/java/org/jenkinsci/main/modules/cli/auth/ssh/CLITest.java b/src/test/java/org/jenkinsci/main/modules/cli/auth/ssh/CLITest.java index f16b43d..39e3c5a 100644 --- a/src/test/java/org/jenkinsci/main/modules/cli/auth/ssh/CLITest.java +++ b/src/test/java/org/jenkinsci/main/modules/cli/auth/ssh/CLITest.java @@ -25,17 +25,19 @@ package org.jenkinsci.main.modules.cli.auth.ssh; import com.gargoylesoftware.htmlunit.WebResponse; +import hudson.Extension; import hudson.Launcher; import hudson.Proc; +import hudson.cli.CLICommand; import hudson.model.FreeStyleProject; import hudson.model.UnprotectedRootAction; import hudson.model.User; import hudson.security.csrf.CrumbExclusion; import hudson.util.StreamTaskListener; -import jenkins.model.GlobalConfiguration; -import jenkins.model.Jenkins; import io.jenkins.cli.shaded.org.apache.commons.io.FileUtils; import io.jenkins.cli.shaded.org.apache.commons.io.output.TeeOutputStream; +import jenkins.model.GlobalConfiguration; +import jenkins.model.Jenkins; import org.apache.commons.io.IOUtils; import org.apache.sshd.common.util.io.ModifiableFileWatcher; import org.jenkinsci.main.modules.sshd.SSHD; @@ -64,17 +66,19 @@ import java.io.IOException; import java.io.PrintWriter; import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; -import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; -import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNull; import static org.junit.Assume.assumeNoException; import static org.junit.Assume.assumeThat; import static org.junit.Assume.assumeTrue; @@ -130,7 +134,7 @@ public void strictHostKey() throws Exception { SSHD.get().setPort(0); File privkey = tmp.newFile("id_rsa"); FileUtils.copyURLToFile(CLITest.class.getResource("id_rsa"), privkey); - User.get("admin").addProperty(new UserPropertyImpl(IOUtils.toString(CLITest.class.getResource("id_rsa.pub")))); + User.getById("admin", true).addProperty(new UserPropertyImpl(IOUtils.toString(CLITest.class.getResource("id_rsa.pub"), StandardCharsets.UTF_8))); assertNotEquals(0, new Launcher.LocalLauncher(StreamTaskListener.fromStderr()).launch().cmds( "java", "-Duser.home=" + home, "-jar", jar.getAbsolutePath(), "-s", r.getURL().toString(), "-ssh", "-user", "admin", "-i", privkey.getAbsolutePath(), "-strictHostKey", "who-am-i" ).stdout(System.out).stderr(System.err).join()); @@ -162,7 +166,7 @@ public void interrupt() throws Exception { SSHD.get().setPort(0); File privkey = tmp.newFile("id_rsa"); FileUtils.copyURLToFile(CLITest.class.getResource("id_rsa"), privkey); - User.get("admin").addProperty(new UserPropertyImpl(IOUtils.toString(CLITest.class.getResource("id_rsa.pub")))); + User.getById("admin", true).addProperty(new UserPropertyImpl(IOUtils.toString(CLITest.class.getResource("id_rsa.pub"), StandardCharsets.UTF_8))); FreeStyleProject p = r.createFreeStyleProject("p"); ByteArrayOutputStream baos = new ByteArrayOutputStream(); p.getBuildersList().add(new SleepBuilder(TimeUnit.MINUTES.toMillis(2))); @@ -179,6 +183,28 @@ public void interrupt() throws Exception { r.waitForCompletion(p.getLastBuild()); } + @Issue("JENKINS-68541") + @Test + public void outputStream() throws Exception { + home = tempHome(); + grabCliJar(); + + r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); + r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().grant(Jenkins.ADMINISTER).everywhere().to("admin")); + SSHD.get().setPort(0); + File privkey = tmp.newFile("id_rsa"); + FileUtils.copyURLToFile(CLITest.class.getResource("id_rsa"), privkey); + User.getById("admin", true).addProperty(new UserPropertyImpl(IOUtils.toString(CLITest.class.getResource("id_rsa.pub"), StandardCharsets.UTF_8))); + StreamTaskListener stl = StreamTaskListener.fromStderr(); + List args = Arrays.asList("java", "-Duser.home=" + home, "-jar", jar.getAbsolutePath(), "-s", r.getURL().toString(), "-ssh", "-user", "admin", "-i", privkey.getAbsolutePath(), "close-stdout-stream"); + int ret = new Launcher.LocalLauncher(stl).launch().cmds(args) + .stdout(System.out) + .stderr(System.err) + .start() + .joinWithTimeout(5, TimeUnit.SECONDS, stl); + assertEquals(0, ret); + } + @Test @Issue("JENKINS-44361") public void reportNotJenkins() throws Exception { home = tempHome(); @@ -241,9 +267,9 @@ public void redirectToEndpointShouldBeFollowed() throws Exception { WebResponse rsp = wc.goTo("cli-proxy/").getWebResponse(); assertEquals(rsp.getContentAsString(), HttpURLConnection.HTTP_MOVED_TEMP, rsp.getStatusCode()); - assertEquals(rsp.getContentAsString(), null, rsp.getResponseHeaderValue("X-Jenkins")); - assertEquals(rsp.getContentAsString(), null, rsp.getResponseHeaderValue("X-Jenkins-CLI-Port")); - assertEquals(rsp.getContentAsString(), null, rsp.getResponseHeaderValue("X-SSH-Endpoint")); + assertNull(rsp.getContentAsString(), rsp.getResponseHeaderValue("X-Jenkins")); + assertNull(rsp.getContentAsString(), rsp.getResponseHeaderValue("X-Jenkins-CLI-Port")); + assertNull(rsp.getContentAsString(), rsp.getResponseHeaderValue("X-SSH-Endpoint")); String url = r.getURL().toString() + "cli-proxy/"; ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -296,4 +322,19 @@ public boolean process(HttpServletRequest request, HttpServletResponse response, return true; } } + + @Extension + public static class CloseStdoutStreamCommand extends CLICommand { + + @Override + public String getShortDescription() { + return "Close stdout"; + } + + @Override + protected int run() { + stdout.close(); + return 0; + } + } } diff --git a/src/test/java/org/jenkinsci/main/modules/cli/auth/ssh/UserPropertyImplTest.java b/src/test/java/org/jenkinsci/main/modules/cli/auth/ssh/UserPropertyImplTest.java index 8ece9f5..cf41908 100644 --- a/src/test/java/org/jenkinsci/main/modules/cli/auth/ssh/UserPropertyImplTest.java +++ b/src/test/java/org/jenkinsci/main/modules/cli/auth/ssh/UserPropertyImplTest.java @@ -25,7 +25,7 @@ public void dsa() throws Exception { private void testRoundtrip(String publicKey) throws Exception { r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); - User foo = User.get("foo"); + User foo = User.getById("foo", true); foo.addProperty(new UserPropertyImpl(publicKey)); r.configRoundtrip(foo); assertEquals(publicKey, foo.getProperty(UserPropertyImpl.class).authorizedKeys);