diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 44c7e2a1f11464b4f5550f6d74f1d6f4cee957fb..0e47cddae6542c7463aad91efb363a5402336b3d 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,16 +1,14 @@
-# Description
-
See [JENKINS-XXXXX](https://issues.jenkins-ci.org/browse/JENKINS-XXXXX).
-Details: TODO
-
+If the issue is not fully described in the ticket, add more information here (justification, pull request links, etc.).
-### Changelog entries
+ * We do not require JIRA issues for minor improvements.
+ * Bugfixes should have a JIRA issue (backporting process).
+ * Major new features should have a JIRA issue reference.
+-->
-Proposed changelog entries:
+### Proposed changelog entries
* Entry 1: Issue, Human-readable Text
* ...
@@ -21,21 +19,17 @@ The changelogs will be integrated by the core maintainers after the merge. See
### Submitter checklist
- [ ] JIRA issue is well described
-- [ ] Link to JIRA ticket in description, if appropriate
+- [ ] Changelog entry appropriate for the audience affected by the change (users or developer, depending on the change). [Examples](https://github.com/jenkins-infra/jenkins.io/blob/master/content/_data/changelogs/weekly.yml)
+ * Use the `Internal: ` prefix if the change has no user-visible impact (API, test frameworks, etc.)
- [ ] Appropriate autotests or explanation to why this change has no tests
-- [ ] For new API and extension points: Link to the reference implementation in open-source (or example in Javadoc)
+- [ ] For dependency updates: links to external changelogs and, if possible, full diffs
-
+
### Desired reviewers
@mention
diff --git a/.gitignore b/.gitignore
index 60ba89065c90d09f0cc1ea8935b0bf08b691b0e9..5c8b1b7ac8b0be6f49db372e82de07e26bdac765 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,9 @@ out
.project
build
+# VSCode workspace file
+*.code-workspace
+
# vim
*~
*.swp
@@ -49,4 +52,5 @@ jenkins_*.changes
*.zip
push-build.sh
war/node_modules/
+war/yarn-error.log
.java-version
diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a2d496cc2b211139d6f9b2eee27cb0be380ec8f5
--- /dev/null
+++ b/.mvn/extensions.xml
@@ -0,0 +1,7 @@
+
+
+ io.jenkins.tools.incrementals
+ git-changelist-maven-extension
+ 1.0-beta-4
+
+
diff --git a/.mvn/maven.config b/.mvn/maven.config
new file mode 100644
index 0000000000000000000000000000000000000000..e54fdacfe62468e4e85ebe51890384926933b476
--- /dev/null
+++ b/.mvn/maven.config
@@ -0,0 +1 @@
+-Pmight-produce-incrementals
diff --git a/BUILDING.TXT b/BUILDING.TXT
deleted file mode 100644
index bde5cadf353210259628baa34b1813f0acddc5b1..0000000000000000000000000000000000000000
--- a/BUILDING.TXT
+++ /dev/null
@@ -1,11 +0,0 @@
-If you want simply to have the jenkins.war as fast as possible (without test
-execution), run:
-
- mvn clean install -pl war -am -DskipTests
-
-The WAR file will be in war/target/jenkins.war (you can play with it)
-
-For more information on building Jenkins, visit
-https://wiki.jenkins-ci.org/display/JENKINS/Building+Jenkins
-
-Have Fun !!
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f99fe77067ef6c25597bf54df9e0debdaff27fef..4311a35ac21abf137bfc69b9415f3f62bb49f8d1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,3 +1,131 @@
# Contributing to Jenkins
-For information on contributing to Jenkins, check out https://jenkins.io/redirect/contribute/. That page will help you get started with contributing to Jenkins.
+This page provides information about contributing code to the Jenkins core codebase.
+
+:exclamation: There's a lot more to the Jenkins project than just code. For information on contributing to the Jenkins project overall, check out [Participate].
+
+## Getting started
+
+1. Fork the repository on GitHub
+2. Clone the forked repository to your machine
+3. Install the development tools. In order to develop Jenkins, you need the following tools:
+ * Java Development Kit (JDK) 8.
+ - In Jenkins project we usually use [OpenJDK],
+ but you can use other JDKs as well.
+ - Java 9+ is **not supported** in Jenkins.
+ * Maven 3.5.3 or above. You can [download maven].
+ * Any IDE which supports importing Maven projects.
+4. Setup your development environment as described in [Preparing for Plugin Development]
+
+If you want to contribute to Jenkins or just learn about the project,
+you can start by fixing some easier issues.
+In the Jenkins issue tracker we mark such issues as `newbie-friendly`.
+You can find them
+using this query for [newbie friendly issues].
+
+## Building and Debugging
+
+The build flow for Jenkins core is built around Maven.
+There is a description of the [building and debugging process].
+
+If you want simply to have the `jenkins.war` file as fast as possible without tests, run:
+
+ mvn clean package -pl war -am -DskipTests -Dfindbugs.skip
+
+The WAR file will be created in `war/target/jenkins.war`.
+After that you can start Jenkins using Java CLI ([guide]).
+If you want to debug this WAR file without using Maven plugins,
+You can just start the executable with [Remote Debug Flags]
+and then attach IDE Debugger to it.
+
+## Testing changes
+
+Jenkins core includes unit and functional tests as a part of the repository.
+
+Functional tests (`test` module) take a while even on server-grade machines.
+Most of the tests will be launched by the continuous integration instance,
+so there is no strict need to run full test suites before proposing a pull request.
+
+There are 3 profiles for tests:
+
+* `light-test` - only unit tests, no functional tests
+* `smoke-test` - run unit tests + a number of functional tests
+* `all-tests` - Runs all tests, with re-run (default)
+
+In addition to the included tests, you can also find extra integration and UI
+tests in the [Acceptance Test Harness (ATH)] repository.
+If you propose complex UI changes, you should create new ATH tests for them.
+
+## Proposing Changes
+
+The Jenkins project source code repositories are hosted at GitHub.
+All proposed changes are submitted and code reviewed using the _GitHub Pull Request_ process.
+
+To submit a pull request:
+
+1. Commit changes and push them to your fork on GitHub.
+It is a good practice is to create branches instead of pushing to master.
+2. In GitHub Web UI click the _New Pull Request_ button
+3. Select `jenkinsci` as _base fork_ and `master` as `base`, then click _Create Pull Request_
+ * We integrate all changes into the master branch towards the Weekly releases
+ * After that the changes may be backported to the current LTS baseline by the LTS Team.
+ Read more about the [backporting process]
+4. Fill in the Pull Request description according to the [proposed template].
+5. Click _Create Pull Request_
+6. Wait for CI results/reviews, process the feedback.
+ * If you do not get feedback after 3 days, feel free to ping `@jenkinsci/code-reviewers` to CC.
+ * Usually we merge pull requests after 2 votes from reviewers or after 1 vote and 1-week delay without extra reviews.
+
+Once your Pull Request is ready to be merged,
+the repository maintainers will integrate it, prepare changelogs and
+ensure it gets released in one of incoming Weekly releases.
+There is no additional action required from pull request authors at this point.
+
+## Copyright
+
+Jenkins core is licensed under [MIT license], with a few exceptions in bundled classes.
+We consider all contributions as MIT unless it's explicitly stated otherwise.
+MIT-incompatible code contributions will be rejected.
+Contributions under MIT-compatible licenses may be also rejected if they are not ultimately necessary.
+
+We **Do NOT** require pull request submitters to sign the [contributor agreement]
+as long as the code is licensed under MIT and merged by one of the contributors with the signed agreement.
+
+We still encourage people to sign the contributor agreement if they intend to submit more than a few pull requests.
+Signing is also a mandatory prerequisite for getting merge/push permissions to core repositories
+and for joining teams like [Jenkins Security Team].
+
+## Continuous Integration
+
+The Jenkins project has a Continuous Integration server... powered by Jenkins, of course.
+It is located at [ci.jenkins.io].
+
+The Jenkins project uses [Jenkins Pipeline] to run builds.
+The code for the core build flow is stored in the [Jenkinsfile] in the repository root.
+If you want to update that build flow (e.g. "add more checks"),
+just submit a pull request.
+
+# Links
+
+* [Jenkins Contribution Landing Page](https://jenkins.io/participate/)
+* [Jenkins IRC Channel](https://jenkins.io/chat/)
+* [Beginners Guide To Contributing](https://wiki.jenkins.io/display/JENKINS/Beginners+Guide+to+Contributing)
+* [List of newbie-friendly issues in the core](https://issues.jenkins-ci.org/issues/?jql=project%20%3D%20JENKINS%20AND%20status%20in%20(Open%2C%20%22In%20Progress%22%2C%20Reopened)%20AND%20component%20%3D%20core%20AND%20labels%20in%20(newbie-friendly))
+
+[download maven]: https://maven.apache.org/download.cgi
+[Preparing for Plugin Development]: https://jenkins.io/doc/developer/tutorial/prepare/
+[newbie friendly issues]: https://issues.jenkins-ci.org/issues/?jql=project%20%3D%20JENKINS%20AND%20status%20in%20(Open%2C%20%22In%20Progress%22%2C%20Reopened)%20AND%20component%20%3D%20core%20AND%20labels%20in%20(newbie-friendly)
+[OpenJDK]: http://openjdk.java.net/
+[Participate]: https://jenkins.io/participate/
+[building and debugging process]: https://jenkins.io/doc/developer/building/
+[guide]: https://wiki.jenkins.io/display/JENKINS/Starting+and+Accessing+Jenkins
+[Remote Debug Flags]: https://stackoverflow.com/questions/975271/remote-debugging-a-java-application
+[Acceptance Test Harness (ATH)]: https://github.com/jenkinsci/acceptance-test-harness
+[backporting process]: https://jenkins.io/download/lts/
+[proposed template]: .github/PULL_REQUEST_TEMPLATE.md
+[MIT license]: ./LICENSE.txt
+[contributor agreement]: https://wiki.jenkins.io/display/JENKINS/Copyright+on+source+code
+[Jenkins Security Team]: https://jenkins.io/security/#team
+[ci.jenkins.io]: https://ci.jenkins.io/
+[Jenkins Pipeline]: https://jenkins.io/doc/book/pipeline/
+[Jenkinsfile]: ./Jenkinsfile
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..d4541bdfcbaa7904cfc9264f6a823d80d1b55992
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,26 @@
+# This is a Dockerfile definition for Experimental Docker builds.
+# DockerHub: https://hub.docker.com/r/jenkins/jenkins-experimental/
+# If you are looking for official images, see https://github.com/jenkinsci/docker
+FROM maven:3.5.4-jdk-8 as builder
+
+COPY .mvn/ /jenkins/src/.mvn/
+COPY cli/ /jenkins/src/cli/
+COPY core/ /jenkins/src/core/
+COPY src/ /jenkins/src/src/
+COPY test/ /jenkins/src/test/
+COPY war/ /jenkins/src/war/
+COPY *.xml /jenkins/src/
+COPY LICENSE.txt /jenkins/src/LICENSE.txt
+COPY licenseCompleter.groovy /jenkins/src/licenseCompleter.groovy
+COPY show-pom-version.rb /jenkins/src/show-pom-version.rb
+
+WORKDIR /jenkins/src/
+RUN mvn clean install --batch-mode -Plight-test
+
+# The image is based on the previous weekly, new changes in jenkinci/docker are not applied
+FROM jenkins/jenkins:latest
+
+LABEL Description="This is an experimental image for the master branch of the Jenkins core" Vendor="Jenkins Project"
+
+COPY --from=builder /jenkins/src/war/target/jenkins.war /usr/share/jenkins/jenkins.war
+ENTRYPOINT ["tini", "--", "/usr/local/bin/jenkins.sh"]
diff --git a/Jenkinsfile b/Jenkinsfile
index d7cc19de84f36db632b2f27f3a3693e57612defe..f6b139aaec9d2ce37672dc1a977fda58decf8ab5 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -13,18 +13,18 @@
def runTests = true
def failFast = false
-// Only keep the 10 most recent builds.
-properties([[$class: 'jenkins.model.BuildDiscarderProperty', strategy: [$class: 'LogRotator',
- numToKeepStr: '50',
- artifactNumToKeepStr: '20']]])
+properties([buildDiscarder(logRotator(numToKeepStr: '50', artifactNumToKeepStr: '3')), durabilityHint('PERFORMANCE_OPTIMIZED')])
// see https://github.com/jenkins-infra/documentation/blob/master/ci.adoc for information on what node types are available
def buildTypes = ['Linux', 'Windows']
+def jdks = [8, 11]
def builds = [:]
for(i = 0; i < buildTypes.size(); i++) {
+for(j = 0; j < jdks.size(); j++) {
def buildType = buildTypes[i]
- builds[buildType] = {
+ def jdk = jdks[j]
+ builds["${buildType}-jdk${jdk}"] = {
node(buildType.toLowerCase()) {
timestamps {
// First stage is actually checking out the source. Since we're using Multibranch
@@ -33,56 +33,89 @@ for(i = 0; i < buildTypes.size(); i++) {
checkout scm
}
+ def changelistF = "${pwd tmp: true}/changelist"
+ def m2repo = "${pwd tmp: true}/m2repo"
+
// Now run the actual build.
stage("${buildType} Build / Test") {
timeout(time: 180, unit: 'MINUTES') {
// See below for what this method does - we're passing an arbitrary environment
// variable to it so that JAVA_OPTS and MAVEN_OPTS are set correctly.
withMavenEnv(["JAVA_OPTS=-Xmx1536m -Xms512m",
- "MAVEN_OPTS=-Xmx1536m -Xms512m"]) {
+ "MAVEN_OPTS=-Xmx1536m -Xms512m"], jdk) {
// Actually run Maven!
- // The -Dmaven.repo.local=${pwd()}/.repository means that Maven will create a
- // .repository directory at the root of the build (which it gets from the
- // pwd() Workflow call) and use that for the local Maven repository.
- def mvnCmd = "mvn -Pdebug -U clean install ${runTests ? '-Dmaven.test.failure.ignore=true' : '-DskipTests'} -V -B -Dmaven.repo.local=${pwd()}/.repository"
+ // -Dmaven.repo.local=… tells Maven to create a subdir in the temporary directory for the local Maven repository
+ def mvnCmd = "mvn -Pdebug -U -Dset.changelist help:evaluate -Dexpression=changelist -Doutput=$changelistF clean install ${runTests ? '-Dmaven.test.failure.ignore' : '-DskipTests'} -V -B -Dmaven.repo.local=$m2repo -s settings-azure.xml -e"
+
if(isUnix()) {
sh mvnCmd
+ sh 'git add . && git diff --exit-code HEAD'
} else {
- bat "$mvnCmd -Duser.name=yay" // INFRA-1032 workaround
+ bat mvnCmd
}
}
}
}
// Once we've built, archive the artifacts and the test results.
- stage("${buildType} Archive Artifacts / Test Results") {
- def files = findFiles(glob: '**/target/*.jar, **/target/*.war, **/target/*.hpi')
- renameFiles(files, buildType.toLowerCase())
-
- archiveArtifacts artifacts: '**/target/*.jar, **/target/*.war, **/target/*.hpi',
- fingerprint: true
+ stage("${buildType} Publishing") {
if (runTests) {
- junit healthScaleFactor: 20.0, testResults: '**/target/surefire-reports/*.xml'
+ junit healthScaleFactor: 20.0, testResults: '*/target/surefire-reports/*.xml'
+ archiveArtifacts allowEmptyArchive: true, artifacts: '**/target/surefire-reports/*.dumpstream'
+ }
+ if (buildType == 'Linux' && jdk == jdks[0]) {
+ def changelist = readFile(changelistF)
+ dir(m2repo) {
+ archiveArtifacts artifacts: "**/*$changelist/*$changelist*",
+ excludes: '**/*.lastUpdated,**/jenkins-test*/',
+ allowEmptyArchive: true, // in case we forgot to reincrementalify
+ fingerprint: true
+ }
}
}
}
}
}
+}}
+
+// TODO: ATH flow now supports Java 8 only, it needs to be reworked (INFRA-1690)
+builds.ath = {
+ node("docker&&highmem") {
+ // Just to be safe
+ deleteDir()
+ def fileUri
+ def metadataPath
+ dir("sources") {
+ checkout scm
+ withMavenEnv(["JAVA_OPTS=-Xmx1536m -Xms512m",
+ "MAVEN_OPTS=-Xmx1536m -Xms512m"], 8) {
+ sh "mvn --batch-mode --show-version -DskipTests -am -pl war package -Dmaven.repo.local=${pwd tmp: true}/m2repo -s settings-azure.xml"
+ }
+ dir("war/target") {
+ fileUri = "file://" + pwd() + "/jenkins.war"
+ }
+ metadataPath = pwd() + "/essentials.yml"
+ }
+ dir("ath") {
+ runATH jenkins: fileUri, metadataFile: metadataPath
+ }
+ }
}
builds.failFast = failFast
parallel builds
+infra.maybePublishIncrementals()
// This method sets up the Maven and JDK tools, puts them in the environment along
// with whatever other arbitrary environment variables we passed in, and runs the
// body we passed in within that environment.
-void withMavenEnv(List envVars = [], def body) {
+void withMavenEnv(List envVars = [], def javaVersion, def body) {
// The names here are currently hardcoded for my test environment. This needs
// to be made more flexible.
// Using the "tool" Workflow call automatically installs those tools on the
// node.
String mvntool = tool name: "mvn", type: 'hudson.tasks.Maven$MavenInstallation'
- String jdktool = tool name: "jdk8", type: 'hudson.model.JDK'
+ String jdktool = tool name: "jdk${javaVersion}", type: 'hudson.model.JDK'
// Set JAVA_HOME, MAVEN_HOME and special PATH variables for the tools we're
// using.
@@ -96,17 +129,3 @@ void withMavenEnv(List envVars = [], def body) {
body.call()
}
}
-
-// This hacky method is used because File is not whitelisted,
-// so we can't use renameTo or friends
-void renameFiles(def files, String prefix) {
- for(i = 0; i < files.length; i++) {
- def newPath = files[i].path.replace(files[i].name, "${prefix}-${files[i].name}")
- def rename = "${files[i].path} ${newPath}"
- if(isUnix()) {
- sh "mv ${rename}"
- } else {
- bat "move ${rename}"
- }
- }
-}
diff --git a/README.md b/README.md
index 6dc6c47ecef715c771fcfdeb9ccd83cc45cf4962..7ef6c08a8ec40120d755931ce679e5ca870c5bda 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ Non-source downloads such as WAR files and several Linux packages can be found o
Our latest and greatest source of Jenkins can be found on [GitHub]. Fork us!
# Contributing to Jenkins
-Follow [contributing](CONTRIBUTING.md) file.
+Follow the [contributing](CONTRIBUTING.md) file.
# News and Website
All information about Jenkins can be found on our [website]. Follow us on Twitter [@jenkinsci].
diff --git a/cli/pom.xml b/cli/pom.xml
index 42955c8b351879181e91b949d83592e608c7eacc..b02df3123ec17c21c9e7c5d8e12d44a9e6eec3ad 100644
--- a/cli/pom.xml
+++ b/cli/pom.xml
@@ -4,8 +4,8 @@
org.jenkins-ci.main
- pom
- 2.54-SNAPSHOT
+ jenkins-parent
+ ${revision}${changelist}cli
@@ -13,6 +13,11 @@
Jenkins cliCommand line interface for Jenkins
+
+
+ Medium
+
+
org.powermock
@@ -21,18 +26,24 @@
org.powermock
- powermock-api-mockito
+ powermock-api-mockito2testorg.kohsukeaccess-modifier-annotation
- 1.7
+
+
+ org.jenkins-ci
+ annotation-indexercommons-codeccommons-codec
- 1.4
+
+
+ commons-io
+ commons-io${project.groupId}
@@ -47,13 +58,39 @@
org.jvnet.localizerlocalizer
- 1.24
+ 1.26
+ org.apache.sshd
+ sshd-core
+ 1.7.0
+ true
+
+
+
+ net.i2p.crypto
+ eddsa
+ 0.3.0
+
+
+ org.slf4j
+ slf4j-jdk14
+ true
+
+ org.jenkins-citrilead-ssh2build214-jenkins-1
+
+ com.google.code.findbugs
+ annotations
+ provided
+
+
+ commons-lang
+ commons-lang
+
@@ -64,17 +101,19 @@
- attached
+ singlepackage
- jar-with-dependencies
+
+ jar-with-dependencies
+ hudson.cli.CLI
- ${build.version}
+ ${project.version}
diff --git a/cli/src/filter/resources/jenkins/cli/jenkins-cli-version.properties b/cli/src/filter/resources/jenkins/cli/jenkins-cli-version.properties
index 39b91895a20a85254399d1f4f0206c6af45ff094..defbd48204e4784090fe0e17e28fe5bc395abf47 100644
--- a/cli/src/filter/resources/jenkins/cli/jenkins-cli-version.properties
+++ b/cli/src/filter/resources/jenkins/cli/jenkins-cli-version.properties
@@ -1 +1 @@
-version=${build.version}
+version=${project.version}
diff --git a/cli/src/main/java/hudson/cli/CLI.java b/cli/src/main/java/hudson/cli/CLI.java
index 4df7c421b6c9d81f8751a3cb716f09ed76d01bbe..5b436b02b085d9ca964561828d4b2fdeafb41715 100644
--- a/cli/src/main/java/hudson/cli/CLI.java
+++ b/cli/src/main/java/hudson/cli/CLI.java
@@ -24,375 +24,66 @@
package hudson.cli;
import hudson.cli.client.Messages;
-import hudson.remoting.Channel;
-import hudson.remoting.NamingThreadFactory;
-import hudson.remoting.PingThread;
-import hudson.remoting.Pipe;
-import hudson.remoting.RemoteInputStream;
-import hudson.remoting.RemoteOutputStream;
-import hudson.remoting.SocketChannelStream;
-import hudson.remoting.SocketOutputStream;
-
-import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.io.PrintStream;
-import java.io.StringReader;
-import java.net.HttpURLConnection;
-import java.net.InetSocketAddress;
-import java.net.Socket;
import java.net.URL;
import java.net.URLConnection;
+import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
-import java.security.PublicKey;
import java.security.SecureRandom;
-import java.security.Signature;
-import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import static java.util.logging.Level.*;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringUtils;
/**
* CLI entry point to Jenkins.
- *
- * @author Kohsuke Kawaguchi
*/
-public class CLI implements AutoCloseable {
- private final ExecutorService pool;
- private final Channel channel;
- private final CliEntryPoint entryPoint;
- private final boolean ownsPool;
- private final List closables = new ArrayList(); // stuff to close in the close method
- private final String httpsProxyTunnel;
- private final String authorization;
-
- public CLI(URL jenkins) throws IOException, InterruptedException {
- this(jenkins,null);
- }
+public class CLI {
- /**
- * @deprecated
- * Use {@link CLIConnectionFactory} to create {@link CLI}
- */
- @Deprecated
- public CLI(URL jenkins, ExecutorService exec) throws IOException, InterruptedException {
- this(jenkins,exec,null);
- }
+ private CLI() {}
/**
- * @deprecated
- * Use {@link CLIConnectionFactory} to create {@link CLI}
+ * Make sure the connection is open against Jenkins server.
+ *
+ * @param c The open connection.
+ * @throws IOException in case of communication problem.
+ * @throws NotTalkingToJenkinsException when connection is not made to Jenkins service.
*/
- @Deprecated
- public CLI(URL jenkins, ExecutorService exec, String httpsProxyTunnel) throws IOException, InterruptedException {
- this(new CLIConnectionFactory().url(jenkins).executorService(exec).httpsProxyTunnel(httpsProxyTunnel));
- }
-
- /*package*/ CLI(CLIConnectionFactory factory) throws IOException, InterruptedException {
- URL jenkins = factory.jenkins;
- this.httpsProxyTunnel = factory.httpsProxyTunnel;
- this.authorization = factory.authorization;
- ExecutorService exec = factory.exec;
-
- String url = jenkins.toExternalForm();
- if(!url.endsWith("/")) url+='/';
-
- ownsPool = exec==null;
- pool = exec!=null ? exec : Executors.newCachedThreadPool(new NamingThreadFactory(Executors.defaultThreadFactory(), "CLI.pool"));
-
- Channel _channel;
- try {
- _channel = connectViaCliPort(jenkins, getCliTcpPort(url));
- } catch (IOException e) {
- LOGGER.log(Level.FINE,"Failed to connect via CLI port. Falling back to HTTP",e);
- try {
- _channel = connectViaHttp(url);
- } catch (IOException e2) {
- e.addSuppressed(e2);
- throw e;
- }
- }
- this.channel = _channel;
-
- // execute the command
- entryPoint = (CliEntryPoint)_channel.waitForRemoteProperty(CliEntryPoint.class.getName());
-
- if(entryPoint.protocolVersion()!=CliEntryPoint.VERSION)
- throw new IOException(Messages.CLI_VersionMismatch());
- }
-
- private Channel connectViaHttp(String url) throws IOException {
- LOGGER.log(FINE, "Trying to connect to {0} via HTTP", url);
- url+="cli";
- URL jenkins = new URL(url);
-
- FullDuplexHttpStream con = new FullDuplexHttpStream(jenkins,authorization);
- Channel ch = new Channel("Chunked connection to "+jenkins,
- pool,con.getInputStream(),con.getOutputStream());
- final long interval = 15*1000;
- final long timeout = (interval * 3) / 4;
- new PingThread(ch,timeout,interval) {
- protected void onDead() {
- // noop. the point of ping is to keep the connection alive
- // as most HTTP servers have a rather short read time out
- }
- }.start();
- return ch;
- }
-
- private Channel connectViaCliPort(URL jenkins, CliPort clip) throws IOException {
- LOGGER.log(FINE, "Trying to connect directly via TCP/IP to {0}", clip.endpoint);
- final Socket s = new Socket();
- // this prevents a connection from silently terminated by the router in between or the other peer
- // and that goes without unnoticed. However, the time out is often very long (for example 2 hours
- // by default in Linux) that this alone is enough to prevent that.
- s.setKeepAlive(true);
- // we take care of buffering on our own
- s.setTcpNoDelay(true);
- OutputStream out;
-
- if (httpsProxyTunnel!=null) {
- String[] tokens = httpsProxyTunnel.split(":");
- LOGGER.log(Level.FINE, "Using HTTP proxy {0}:{1} to connect to CLI port", new Object[]{tokens[0], tokens[1]});
- s.connect(new InetSocketAddress(tokens[0], Integer.parseInt(tokens[1])));
- PrintStream o = new PrintStream(s.getOutputStream());
- o.print("CONNECT " + clip.endpoint.getHostString() + ":" + clip.endpoint.getPort() + " HTTP/1.0\r\n\r\n");
-
- // read the response from the proxy
- ByteArrayOutputStream rsp = new ByteArrayOutputStream();
- while (!rsp.toString("ISO-8859-1").endsWith("\r\n\r\n")) {
- int ch = s.getInputStream().read();
- if (ch<0) throw new IOException("Failed to read the HTTP proxy response: "+rsp);
- rsp.write(ch);
- }
- String head = new BufferedReader(new StringReader(rsp.toString("ISO-8859-1"))).readLine();
-
- if (head == null) {
- throw new IOException("Unexpected empty response");
- }
- if (!(head.startsWith("HTTP/1.0 200 ") || head.startsWith("HTTP/1.1 200 "))) {
- s.close();
- LOGGER.log(Level.SEVERE, "Failed to tunnel the CLI port through the HTTP proxy. Falling back to HTTP.");
- throw new IOException("Failed to establish a connection through HTTP proxy: " + rsp);
- }
-
- // HTTP proxies (at least the one I tried --- squid) doesn't seem to do half-close very well.
- // So instead of relying on it, we'll just send the close command and then let the server
- // cut their side, then close the socket after the join.
- out = new SocketOutputStream(s) {
- @Override
- public void close() throws IOException {
- // ignore
- }
- };
- } else {
- s.connect(clip.endpoint,3000);
- out = SocketChannelStream.out(s);
- }
-
- closables.add(new Closeable() {
- public void close() throws IOException {
- s.close();
- }
- });
-
- Connection c = new Connection(SocketChannelStream.in(s),out);
-
- switch (clip.version) {
- case 1:
- DataOutputStream dos = new DataOutputStream(s.getOutputStream());
- dos.writeUTF("Protocol:CLI-connect");
- // we aren't checking greeting from the server here because I'm too lazy. It gets ignored by Channel constructor.
- break;
- case 2:
- DataInputStream dis = new DataInputStream(s.getInputStream());
- dos = new DataOutputStream(s.getOutputStream());
- dos.writeUTF("Protocol:CLI2-connect");
- String greeting = dis.readUTF();
- if (!greeting.equals("Welcome"))
- throw new IOException("Handshaking failed: "+greeting);
- try {
- byte[] secret = c.diffieHellman(false).generateSecret();
- SecretKey sessionKey = new SecretKeySpec(Connection.fold(secret,128/8),"AES");
- c = c.encryptConnection(sessionKey,"AES/CFB8/NoPadding");
-
- // validate the instance identity, so that we can be sure that we are talking to the same server
- // and there's no one in the middle.
- byte[] signature = c.readByteArray();
-
- if (clip.identity!=null) {
- Signature verifier = Signature.getInstance("SHA1withRSA");
- verifier.initVerify(clip.getIdentity());
- verifier.update(secret);
- if (!verifier.verify(signature))
- throw new IOException("Server identity signature validation failed.");
- }
-
- } catch (GeneralSecurityException e) {
- throw (IOException)new IOException("Failed to negotiate transport security").initCause(e);
- }
- }
-
- return new Channel("CLI connection to "+jenkins, pool,
- new BufferedInputStream(c.in), new BufferedOutputStream(c.out));
+ /*package*/ static void verifyJenkinsConnection(URLConnection c) throws IOException {
+ if (c.getHeaderField("X-Hudson")==null && c.getHeaderField("X-Jenkins")==null)
+ throw new NotTalkingToJenkinsException(c);
}
-
- /**
- * If the server advertises CLI endpoint, returns its location.
- */
- protected CliPort getCliTcpPort(String url) throws IOException {
- URL _url = new URL(url);
- if (_url.getHost()==null || _url.getHost().length()==0) {
- throw new IOException("Invalid URL: "+url);
- }
- URLConnection head = _url.openConnection();
- try {
- head.connect();
- } catch (IOException e) {
- throw (IOException)new IOException("Failed to connect to "+url).initCause(e);
+ /*package*/ static final class NotTalkingToJenkinsException extends IOException {
+ public NotTalkingToJenkinsException(String s) {
+ super(s);
}
- String h = head.getHeaderField("X-Jenkins-CLI-Host");
- if (h==null) h = head.getURL().getHost();
- String p1 = head.getHeaderField("X-Jenkins-CLI-Port");
- if (p1==null) p1 = head.getHeaderField("X-Hudson-CLI-Port"); // backward compatibility
- String p2 = head.getHeaderField("X-Jenkins-CLI2-Port");
-
- String identity = head.getHeaderField("X-Instance-Identity");
-
- flushURLConnection(head);
- if (p1==null && p2==null) {
- // we aren't finding headers we are expecting. Is this even running Jenkins?
- if (head.getHeaderField("X-Hudson")==null && head.getHeaderField("X-Jenkins")==null)
- throw new IOException("There's no Jenkins running at "+url);
-
- throw new IOException("No X-Jenkins-CLI2-Port among " + head.getHeaderFields().keySet());
- }
-
- if (p2!=null) return new CliPort(new InetSocketAddress(h,Integer.parseInt(p2)),identity,2);
- else return new CliPort(new InetSocketAddress(h,Integer.parseInt(p1)),identity,1);
- }
-
- /**
- * Flush the supplied {@link URLConnection} input and close the
- * connection nicely.
- * @param conn the connection to flush/close
- */
- private void flushURLConnection(URLConnection conn) {
- byte[] buf = new byte[1024];
- try {
- InputStream is = conn.getInputStream();
- while (is.read(buf) >= 0) {
- // Ignore
- }
- is.close();
- } catch (IOException e) {
- try {
- InputStream es = ((HttpURLConnection)conn).getErrorStream();
- if (es!=null) {
- while (es.read(buf) >= 0) {
- // Ignore
- }
- es.close();
- }
- } catch (IOException ex) {
- // Ignore
- }
+ public NotTalkingToJenkinsException(URLConnection c) {
+ super("There's no Jenkins running at " + c.getURL().toString());
}
}
- /**
- * Shuts down the channel and closes the underlying connection.
- */
- public void close() throws IOException, InterruptedException {
- channel.close();
- channel.join();
- if(ownsPool)
- pool.shutdown();
- for (Closeable c : closables)
- c.close();
- }
-
- public int execute(List args, InputStream stdin, OutputStream stdout, OutputStream stderr) {
- return entryPoint.main(args, Locale.getDefault(),
- new RemoteInputStream(stdin),
- new RemoteOutputStream(stdout),
- new RemoteOutputStream(stderr));
- }
-
- public int execute(List args) {
- return execute(args, System.in, System.out, System.err);
- }
-
- public int execute(String... args) {
- return execute(Arrays.asList(args));
- }
-
- /**
- * Returns true if the named command exists.
- */
- public boolean hasCommand(String name) {
- return entryPoint.hasCommand(name);
- }
-
- /**
- * Accesses the underlying communication channel.
- * @since 1.419
- */
- public Channel getChannel() {
- return channel;
- }
-
- /**
- * Attempts to lift the security restriction on the underlying channel.
- * This requires the administer privilege on the server.
- *
- * @throws SecurityException
- * If we fail to upgrade the connection.
- */
- public void upgrade() {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- if (execute(Arrays.asList("groovy", "="),
- new ByteArrayInputStream("hudson.remoting.Channel.current().setRestricted(false)".getBytes()),
- out,out)!=0)
- throw new SecurityException(out.toString()); // failed to upgrade
- }
-
public static void main(final String[] _args) throws Exception {
- Logger l = Logger.getLogger(ROOT_LOGGER_NAME);
- l.setLevel(SEVERE);
- ConsoleHandler h = new ConsoleHandler();
- h.setLevel(SEVERE);
- l.addHandler(h);
-
try {
System.exit(_main(_args));
+ } catch (NotTalkingToJenkinsException ex) {
+ System.err.println(ex.getMessage());
+ System.exit(3);
} catch (Throwable t) {
// if the CLI main thread die, make sure to kill the JVM.
t.printStackTrace();
@@ -400,11 +91,10 @@ public class CLI implements AutoCloseable {
}
}
+ private enum Mode {HTTP, SSH}
public static int _main(String[] _args) throws Exception {
List args = Arrays.asList(_args);
PrivateKeyProvider provider = new PrivateKeyProvider();
- boolean sshAuthRequestedExplicitly = false;
- String httpProxy=null;
String url = System.getenv("JENKINS_URL");
@@ -413,19 +103,51 @@ public class CLI implements AutoCloseable {
boolean tryLoadPKey = true;
+ Mode mode = null;
+
+ String user = null;
+ String auth = null;
+
+ String userIdEnv = System.getenv("JENKINS_USER_ID");
+ String tokenEnv = System.getenv("JENKINS_API_TOKEN");
+
+ boolean strictHostKey = false;
+
while(!args.isEmpty()) {
String head = args.get(0);
if (head.equals("-version")) {
System.out.println("Version: "+computeVersion());
return 0;
}
+ if (head.equals("-http")) {
+ if (mode != null) {
+ printUsage("-http clashes with previously defined mode " + mode);
+ return -1;
+ }
+ mode = Mode.HTTP;
+ args = args.subList(1, args.size());
+ continue;
+ }
+ if (head.equals("-ssh")) {
+ if (mode != null) {
+ printUsage("-ssh clashes with previously defined mode " + mode);
+ return -1;
+ }
+ mode = Mode.SSH;
+ args = args.subList(1, args.size());
+ continue;
+ }
+ if (head.equals("-remoting")) {
+ printUsage("-remoting mode is no longer supported");
+ return -1;
+ }
if(head.equals("-s") && args.size()>=2) {
url = args.get(1);
args = args.subList(2,args.size());
continue;
}
if (head.equals("-noCertificateCheck")) {
- System.err.println("Skipping HTTPS certificate checks altogether. Note that this is not secure at all.");
+ LOGGER.info("Skipping HTTPS certificate checks altogether. Note that this is not secure at all.");
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[]{new NoCheckTrustManager()}, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
@@ -453,21 +175,32 @@ public class CLI implements AutoCloseable {
provider.readFrom(f);
args = args.subList(2,args.size());
- sshAuthRequestedExplicitly = true;
continue;
}
- if(head.equals("-p") && args.size()>=2) {
- httpProxy = args.get(1);
- args = args.subList(2,args.size());
+ if (head.equals("-strictHostKey")) {
+ strictHostKey = true;
+ args = args.subList(1, args.size());
continue;
}
- if(head.equals("-v")) {
- args = args.subList(1,args.size());
- Logger l = Logger.getLogger(ROOT_LOGGER_NAME);
- l.setLevel(FINEST);
- for (Handler h : l.getHandlers()) {
- h.setLevel(FINEST);
+ if (head.equals("-user") && args.size() >= 2) {
+ user = args.get(1);
+ args = args.subList(2, args.size());
+ continue;
+ }
+ if (head.equals("-auth") && args.size() >= 2) {
+ auth = args.get(1);
+ args = args.subList(2, args.size());
+ continue;
+ }
+ if (head.equals("-logger") && args.size() >= 2) {
+ Level level = parse(args.get(1));
+ for (Handler h : Logger.getLogger("").getHandlers()) {
+ h.setLevel(level);
+ }
+ for (Logger logger : new Logger[] {LOGGER, FullDuplexHttpStream.LOGGER, PlainCLIProtocol.LOGGER, Logger.getLogger("org.apache.sshd")}) { // perhaps also Channel
+ logger.setLevel(level);
}
+ args = args.subList(2, args.size());
continue;
}
break;
@@ -478,51 +211,148 @@ public class CLI implements AutoCloseable {
return -1;
}
+ if (auth == null) {
+ // -auth option not set
+ if (StringUtils.isNotBlank(userIdEnv) && StringUtils.isNotBlank(tokenEnv)) {
+ auth = StringUtils.defaultString(userIdEnv).concat(":").concat(StringUtils.defaultString(tokenEnv));
+ } else if (StringUtils.isNotBlank(userIdEnv) || StringUtils.isNotBlank(tokenEnv)) {
+ printUsage(Messages.CLI_BadAuth());
+ return -1;
+ } // Otherwise, none credentials were set
+
+ }
+
+ if (!url.endsWith("/")) {
+ url += '/';
+ }
+
if(args.isEmpty())
args = Arrays.asList("help"); // default to help
if (tryLoadPKey && !provider.hasKeys())
provider.readFromDefaultLocations();
- CLIConnectionFactory factory = new CLIConnectionFactory().url(url).httpsProxyTunnel(httpProxy);
+ if (mode == null) {
+ mode = Mode.HTTP;
+ }
+
+ LOGGER.log(FINE, "using connection mode {0}", mode);
+
+ if (user != null && auth != null) {
+ LOGGER.warning("-user and -auth are mutually exclusive");
+ }
+
+ if (mode == Mode.SSH) {
+ if (user == null) {
+ // TODO SshCliAuthenticator already autodetects the user based on public key; why cannot AsynchronousCommand.getCurrentUser do the same?
+ LOGGER.warning("-user required when using -ssh");
+ return -1;
+ }
+ return SSHCLI.sshConnection(url, user, args, provider, strictHostKey);
+ }
+
+ if (strictHostKey) {
+ LOGGER.warning("-strictHostKey meaningful only with -ssh");
+ }
+
+ if (user != null) {
+ LOGGER.warning("Warning: -user ignored unless using -ssh");
+ }
+
+ CLIConnectionFactory factory = new CLIConnectionFactory();
String userInfo = new URL(url).getUserInfo();
if (userInfo != null) {
factory = factory.basicAuth(userInfo);
+ } else if (auth != null) {
+ factory = factory.basicAuth(auth.startsWith("@") ? FileUtils.readFileToString(new File(auth.substring(1))).trim() : auth);
}
- CLI cli = factory.connect();
- try {
- if (provider.hasKeys()) {
- try {
- // TODO: server verification
- cli.authenticate(provider.getKeys());
- } catch (IllegalStateException e) {
- if (sshAuthRequestedExplicitly) {
- System.err.println("The server doesn't support public key authentication");
- return -1;
- }
- } catch (UnsupportedOperationException e) {
- if (sshAuthRequestedExplicitly) {
- System.err.println("The server doesn't support public key authentication");
- return -1;
+ if (mode == Mode.HTTP) {
+ return plainHttpConnection(url, args, factory);
+ }
+
+ throw new AssertionError();
+ }
+
+ private static int plainHttpConnection(String url, List args, CLIConnectionFactory factory) throws IOException, InterruptedException {
+ LOGGER.log(FINE, "Trying to connect to {0} via plain protocol over HTTP", url);
+ FullDuplexHttpStream streams = new FullDuplexHttpStream(new URL(url), "cli?remoting=false", factory.authorization);
+ class ClientSideImpl extends PlainCLIProtocol.ClientSide {
+ boolean complete;
+ int exit = -1;
+ ClientSideImpl(InputStream is, OutputStream os) throws IOException {
+ super(is, os);
+ if (is.read() != 0) { // cf. FullDuplexHttpService
+ throw new IOException("expected to see initial zero byte; perhaps you are connecting to an old server which does not support -http?");
+ }
+ }
+ @Override
+ protected void onExit(int code) {
+ this.exit = code;
+ finished();
+ }
+ @Override
+ protected void onStdout(byte[] chunk) throws IOException {
+ System.out.write(chunk);
+ }
+ @Override
+ protected void onStderr(byte[] chunk) throws IOException {
+ System.err.write(chunk);
+ }
+ @Override
+ protected void handleClose() {
+ finished();
+ }
+ private synchronized void finished() {
+ complete = true;
+ notifyAll();
+ }
+ }
+ try (final ClientSideImpl connection = new ClientSideImpl(streams.getInputStream(), streams.getOutputStream())) {
+ for (String arg : args) {
+ connection.sendArg(arg);
+ }
+ connection.sendEncoding(Charset.defaultCharset().name());
+ connection.sendLocale(Locale.getDefault().toString());
+ connection.sendStart();
+ connection.begin();
+ new Thread("input reader") {
+ @Override
+ public void run() {
+ try {
+ final OutputStream stdin = connection.streamStdin();
+ int c;
+ while (!connection.complete && (c = System.in.read()) != -1) {
+ stdin.write(c);
+ }
+ connection.sendEndStdin();
+ } catch (IOException x) {
+ LOGGER.log(Level.WARNING, null, x);
}
- } catch (GeneralSecurityException e) {
- if (sshAuthRequestedExplicitly) {
- System.err.println(e.getMessage());
- LOGGER.log(FINE,e.getMessage(),e);
- return -1;
+ }
+ }.start();
+ new Thread("ping") { // JENKINS-46659
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(10_000);
+ while (!connection.complete) {
+ LOGGER.fine("sending ping");
+ connection.sendEncoding(Charset.defaultCharset().name()); // no-op at this point
+ Thread.sleep(10_000);
+ }
+ } catch (IOException | InterruptedException x) {
+ LOGGER.log(Level.WARNING, null, x);
}
- System.err.println("[WARN] Failed to authenticate with your SSH keys. Proceeding as anonymous");
- LOGGER.log(FINE,"Failed to authenticate with your SSH keys.",e);
}
- }
- // execute the command
- // Arrays.asList is not serializable --- see 6835580
- args = new ArrayList(args);
- return cli.execute(args, System.in, System.out, System.err);
- } finally {
- cli.close();
+ }.start();
+ synchronized (connection) {
+ while (!connection.complete) {
+ connection.wait();
+ }
+ }
+ return connection.exit;
}
}
@@ -565,46 +395,15 @@ public class CLI implements AutoCloseable {
return loadKey(pemString, null);
}
- /**
- * Authenticate ourselves against the server.
- *
- * @return
- * identity of the server represented as a public key.
- */
- public PublicKey authenticate(Iterable privateKeys) throws IOException, GeneralSecurityException {
- Pipe c2s = Pipe.createLocalToRemote();
- Pipe s2c = Pipe.createRemoteToLocal();
- entryPoint.authenticate("ssh",c2s, s2c);
- Connection c = new Connection(s2c.getIn(), c2s.getOut());
-
- try {
- byte[] sharedSecret = c.diffieHellman(false).generateSecret();
- PublicKey serverIdentity = c.verifyIdentity(sharedSecret);
-
- // try all the public keys
- for (KeyPair key : privateKeys) {
- c.proveIdentity(sharedSecret,key);
- if (c.readBoolean())
- return serverIdentity; // succeeded
- }
- if (privateKeys.iterator().hasNext())
- throw new GeneralSecurityException("Authentication failed. No private key accepted.");
- else
- throw new GeneralSecurityException("No private key is available for use in authentication");
- } finally {
- c.close();
- }
- }
-
- public PublicKey authenticate(KeyPair key) throws IOException, GeneralSecurityException {
- return authenticate(Collections.singleton(key));
+ /** For access from {@code HelpCommand}. */
+ static String usage() {
+ return Messages.CLI_Usage();
}
private static void printUsage(String msg) {
if(msg!=null) System.out.println(msg);
- System.err.println(Messages.CLI_Usage());
+ System.err.println(usage());
}
- private static final Logger LOGGER = Logger.getLogger(CLI.class.getName());
- private static final String ROOT_LOGGER_NAME = CLI.class.getPackage().getName();
+ static final Logger LOGGER = Logger.getLogger(CLI.class.getName());
}
diff --git a/cli/src/main/java/hudson/cli/CLIConnectionFactory.java b/cli/src/main/java/hudson/cli/CLIConnectionFactory.java
index a2e5681039effafbb22765a249a508fe8fdb22b2..288696b0da0828be52b2bd7c8cafd62e90719914 100644
--- a/cli/src/main/java/hudson/cli/CLIConnectionFactory.java
+++ b/cli/src/main/java/hudson/cli/CLIConnectionFactory.java
@@ -2,10 +2,6 @@ package hudson.cli;
import org.apache.commons.codec.binary.Base64;
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.concurrent.ExecutorService;
/**
* Fluent-API to instantiate {@link CLI}.
@@ -13,40 +9,8 @@ import java.util.concurrent.ExecutorService;
* @author Kohsuke Kawaguchi
*/
public class CLIConnectionFactory {
- URL jenkins;
- ExecutorService exec;
- String httpsProxyTunnel;
String authorization;
- /**
- * Top URL of the Jenkins to connect to.
- */
- public CLIConnectionFactory url(URL jenkins) {
- this.jenkins = jenkins;
- return this;
- }
-
- public CLIConnectionFactory url(String jenkins) throws MalformedURLException {
- return url(new URL(jenkins));
- }
-
- /**
- * This {@link ExecutorService} is used to execute closures received from the server.
- */
- public CLIConnectionFactory executorService(ExecutorService es) {
- this.exec = es;
- return this;
- }
-
- /**
- * Configures the HTTP proxy that we use for making a plain TCP/IP connection.
- * "host:port" that points to an HTTP proxy or null.
- */
- public CLIConnectionFactory httpsProxyTunnel(String value) {
- this.httpsProxyTunnel = value;
- return this;
- }
-
/**
* For CLI connection that goes through HTTP, sometimes you need
* to pass in the custom authentication header (before Jenkins even get to authenticate
@@ -59,16 +23,18 @@ public class CLIConnectionFactory {
/**
* Convenience method to call {@link #authorization} with the HTTP basic authentication.
+ * Currently unused.
*/
public CLIConnectionFactory basicAuth(String username, String password) {
return basicAuth(username+':'+password);
}
+ /**
+ * Convenience method to call {@link #authorization} with the HTTP basic authentication.
+ * Cf. {@code BasicHeaderApiTokenAuthenticator}.
+ */
public CLIConnectionFactory basicAuth(String userInfo) {
return authorization("Basic " + new String(Base64.encodeBase64((userInfo).getBytes())));
}
-
- public CLI connect() throws IOException, InterruptedException {
- return new CLI(this);
- }
+
}
diff --git a/cli/src/main/java/hudson/cli/CliEntryPoint.java b/cli/src/main/java/hudson/cli/CliEntryPoint.java
deleted file mode 100644
index 57bd42e553b77336c2a0db0085ae4ce0474c76b2..0000000000000000000000000000000000000000
--- a/cli/src/main/java/hudson/cli/CliEntryPoint.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * The MIT License
- *
- * Copyright (c) 2004-2009, Sun Microsystems, 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 hudson.cli;
-
-import hudson.remoting.Pipe;
-
-import java.io.OutputStream;
-import java.io.InputStream;
-import java.util.List;
-import java.util.Locale;
-
-/**
- * Remotable interface for CLI entry point on the server side.
- *
- * @author Kohsuke Kawaguchi
- */
-public interface CliEntryPoint {
- /**
- * Just like the static main method.
- *
- * @param locale
- * Locale of this client.
- */
- int main(List args, Locale locale, InputStream stdin, OutputStream stdout, OutputStream stderr);
-
- /**
- * Does the named command exist?
- */
- boolean hasCommand(String name);
-
- /**
- * Returns {@link #VERSION}, so that the client and the server can detect version incompatibility
- * gracefully.
- */
- int protocolVersion();
-
- /**
- * Initiates authentication out of band.
- *
- * This method starts two-way byte channel that allows the client and the server to perform authentication.
- * The current supported implementation is based on SSH public key authentication that mutually authenticates
- * clients and servers.
- *
- * @param protocol
- * Currently only "ssh" is supported.
- * @throws UnsupportedOperationException
- * If the specified protocol is not supported by the server.
- */
- void authenticate(String protocol, Pipe c2s, Pipe s2c);
-
- int VERSION = 1;
-}
diff --git a/cli/src/main/java/hudson/cli/CliPort.java b/cli/src/main/java/hudson/cli/CliPort.java
deleted file mode 100644
index 8faab7de41bee0c88fa90cdaa0cbb8c9599f6a97..0000000000000000000000000000000000000000
--- a/cli/src/main/java/hudson/cli/CliPort.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package hudson.cli;
-
-import org.apache.commons.codec.binary.Base64;
-
-import java.net.InetSocketAddress;
-import java.security.GeneralSecurityException;
-import java.security.KeyFactory;
-import java.security.PublicKey;
-import java.security.spec.X509EncodedKeySpec;
-
-/**
- * @author Kohsuke Kawaguchi
- */
-public final class CliPort {
- /**
- * The TCP endpoint to talk to.
- */
- final InetSocketAddress endpoint;
-
- /**
- * CLI protocol version. 1 and 2 are currently defined.
- */
- final int version;
-
- /**
- * Server instance identity. Can be null.
- */
- final String identity;
-
- public CliPort(InetSocketAddress endpoint, String identity, int version) {
- this.endpoint = endpoint;
- this.identity = identity;
- this.version = version;
- }
-
- /**
- * Gets the public part of the RSA key that represents the server identity.
- */
- public PublicKey getIdentity() throws GeneralSecurityException {
- if (identity==null) return null;
- byte[] image = Base64.decodeBase64(identity);
- return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(image));
- }
-}
diff --git a/cli/src/main/java/hudson/cli/Connection.java b/cli/src/main/java/hudson/cli/Connection.java
index 1c1ada471fdbaaded6f2203ec130a7e69a671a6f..1961447778d04ab829c9b550305ac016309bcd35 100644
--- a/cli/src/main/java/hudson/cli/Connection.java
+++ b/cli/src/main/java/hudson/cli/Connection.java
@@ -55,7 +55,12 @@ import java.security.Signature;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;
+import org.jenkinsci.remoting.util.AnonymousClassWarnings;
+/**
+ * @deprecated No longer used.
+ */
+@Deprecated
public class Connection {
public final InputStream in;
public final OutputStream out;
@@ -99,7 +104,7 @@ public class Connection {
* Sends a serializable object.
*/
public void writeObject(Object o) throws IOException {
- ObjectOutputStream oos = new ObjectOutputStream(out);
+ ObjectOutputStream oos = AnonymousClassWarnings.checkingObjectOutputStream(out);
oos.writeObject(o);
// don't close oss, which will close the underlying stream
// no need to flush either, given the way oos is implemented
diff --git a/cli/src/main/java/hudson/cli/DiagnosedStreamCorruptionException.java b/cli/src/main/java/hudson/cli/DiagnosedStreamCorruptionException.java
new file mode 100644
index 0000000000000000000000000000000000000000..4708b425dbb77a715910ac28c67ec84aca79db0f
--- /dev/null
+++ b/cli/src/main/java/hudson/cli/DiagnosedStreamCorruptionException.java
@@ -0,0 +1,55 @@
+package hudson.cli;
+
+import java.io.PrintWriter;
+import java.io.StreamCorruptedException;
+import java.io.StringWriter;
+
+// TODO COPIED FROM hudson.remoting
+
+/**
+ * Signals a {@link StreamCorruptedException} with some additional diagnostic information.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+class DiagnosedStreamCorruptionException extends StreamCorruptedException {
+ private final Exception diagnoseFailure;
+ private final byte[] readBack;
+ private final byte[] readAhead;
+
+ DiagnosedStreamCorruptionException(Exception cause, Exception diagnoseFailure, byte[] readBack, byte[] readAhead) {
+ initCause(cause);
+ this.diagnoseFailure = diagnoseFailure;
+ this.readBack = readBack;
+ this.readAhead = readAhead;
+ }
+
+ public Exception getDiagnoseFailure() {
+ return diagnoseFailure;
+ }
+
+ public byte[] getReadBack() {
+ return readBack;
+ }
+
+ public byte[] getReadAhead() {
+ return readAhead;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buf = new StringBuilder();
+ buf.append(super.toString()).append("\n");
+ buf.append("Read back: ").append(HexDump.toHex(readBack)).append('\n');
+ buf.append("Read ahead: ").append(HexDump.toHex(readAhead));
+ if (diagnoseFailure!=null) {
+ StringWriter w = new StringWriter();
+ PrintWriter p = new PrintWriter(w);
+ diagnoseFailure.printStackTrace(p);
+ p.flush();
+
+ buf.append("\nDiagnosis problem:\n ");
+ buf.append(w.toString().trim().replace("\n","\n "));
+ }
+ return buf.toString();
+ }
+}
diff --git a/cli/src/main/java/hudson/cli/FlightRecorderInputStream.java b/cli/src/main/java/hudson/cli/FlightRecorderInputStream.java
new file mode 100644
index 0000000000000000000000000000000000000000..ebdd18a192fec4d2e29e25c85dbeb0c2679e40d6
--- /dev/null
+++ b/cli/src/main/java/hudson/cli/FlightRecorderInputStream.java
@@ -0,0 +1,191 @@
+package hudson.cli;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+// TODO COPIED FROM hudson.remoting
+
+/**
+ * Filter input stream that records the content as it's read, so that it can be reported
+ * in case of a catastrophic stream corruption problem.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+class FlightRecorderInputStream extends InputStream {
+
+ /**
+ * Size (in bytes) of the flight recorder ring buffer used for debugging remoting issues.
+ * @since 2.41
+ */
+ static final int BUFFER_SIZE = Integer.getInteger("hudson.remoting.FlightRecorderInputStream.BUFFER_SIZE", 1024 * 1024);
+
+ private final InputStream source;
+ private ByteArrayRingBuffer recorder = new ByteArrayRingBuffer(BUFFER_SIZE);
+
+ FlightRecorderInputStream(InputStream source) {
+ this.source = source;
+ }
+
+ /**
+ * Rewinds the record buffer and forget everything that was recorded.
+ */
+ public void clear() {
+ recorder = new ByteArrayRingBuffer(BUFFER_SIZE);
+ }
+
+ /**
+ * Gets the recorded content.
+ */
+ public byte[] getRecord() {
+ return recorder.toByteArray();
+ }
+
+ /**
+ * Creates a {@link DiagnosedStreamCorruptionException} based on the recorded content plus read ahead.
+ * The caller is responsible for throwing the exception.
+ */
+ public DiagnosedStreamCorruptionException analyzeCrash(Exception problem, String diagnosisName) {
+ final ByteArrayOutputStream readAhead = new ByteArrayOutputStream();
+ final IOException[] error = new IOException[1];
+
+ Thread diagnosisThread = new Thread(diagnosisName+" stream corruption diagnosis thread") {
+ public void run() {
+ int b;
+ try {
+ // not all InputStream will look for the thread interrupt flag, so check that explicitly to be defensive
+ while (!Thread.interrupted() && (b=source.read())!=-1) {
+ readAhead.write(b);
+ }
+ } catch (IOException e) {
+ error[0] = e;
+ }
+ }
+ };
+
+ // wait up to 1 sec to grab as much data as possible
+ diagnosisThread.start();
+ try {
+ diagnosisThread.join(1000);
+ } catch (InterruptedException ignored) {
+ // we are only waiting for a fixed amount of time, so we'll pretend like we were in a busy loop
+ Thread.currentThread().interrupt();
+ // fall through
+ }
+
+ IOException diagnosisProblem = error[0]; // capture the error, if any, before we kill the thread
+ if (diagnosisThread.isAlive())
+ diagnosisThread.interrupt(); // if it's not dead, kill
+
+ return new DiagnosedStreamCorruptionException(problem,diagnosisProblem,getRecord(),readAhead.toByteArray());
+
+ }
+
+ @Override
+ public int read() throws IOException {
+ int i = source.read();
+ if (i>=0)
+ recorder.write(i);
+ return i;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ len = source.read(b, off, len);
+ if (len>0)
+ recorder.write(b,off,len);
+ return len;
+ }
+
+ /**
+ * To record the bytes we've skipped, convert the call to read.
+ */
+ @Override
+ public long skip(long n) throws IOException {
+ byte[] buf = new byte[(int)Math.min(n,64*1024)];
+ return read(buf,0,buf.length);
+ }
+
+ @Override
+ public int available() throws IOException {
+ return source.available();
+ }
+
+ @Override
+ public void close() throws IOException {
+ source.close();
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ // http://stackoverflow.com/a/3651696/12916
+ private static class ByteArrayRingBuffer extends OutputStream {
+
+ byte[] data;
+
+ int capacity, pos = 0;
+
+ boolean filled = false;
+
+ public ByteArrayRingBuffer(int capacity) {
+ data = new byte[capacity];
+ this.capacity = capacity;
+ }
+
+ @Override
+ public synchronized void write(int b) {
+ if (pos == capacity) {
+ filled = true;
+ pos = 0;
+ }
+ data[pos++] = (byte) b;
+ }
+
+ public synchronized byte[] toByteArray() {
+ if (!filled) {
+ return Arrays.copyOf(data, pos);
+ }
+ byte[] ret = new byte[capacity];
+ System.arraycopy(data, pos, ret, 0, capacity - pos);
+ System.arraycopy(data, 0, ret, capacity - pos, pos);
+ return ret;
+ }
+
+ /** @author @roadrunner2 */
+ @Override public synchronized void write(byte[] buf, int off, int len) {
+ // no point in trying to copy more than capacity; this also simplifies logic below
+ if (len > capacity) {
+ off += (len - capacity);
+ len = capacity;
+ }
+
+ // copy to buffer, but no farther than the end
+ int num = Math.min(len, capacity - pos);
+ if (num > 0) {
+ System.arraycopy(buf, off, data, pos, num);
+ off += num;
+ len -= num;
+ pos += num;
+ }
+
+ // wrap around if necessary
+ if (pos == capacity) {
+ filled = true;
+ pos = 0;
+ }
+
+ // copy anything still left
+ if (len > 0) {
+ System.arraycopy(buf, off, data, pos, len);
+ pos += len;
+ }
+ }
+
+ }
+
+}
diff --git a/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java b/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java
index 3c7911e1723d0191340dcd457af46a23fbb5a65e..723e97f16c26805a3550a0d9b8fcb17476396fee 100644
--- a/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java
+++ b/cli/src/main/java/hudson/cli/FullDuplexHttpStream.java
@@ -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;
@@ -11,58 +9,57 @@ import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
-import org.apache.commons.codec.binary.Base64;
-
/**
* 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 {
- private final URL target;
- /**
- * Authorization header value needed to get through the HTTP layer.
- */
- private final String authorization;
+ private final URL base;
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;
}
- @Deprecated
- public FullDuplexHttpStream(URL target) throws IOException {
- this(target,basicAuth(target.getUserInfo()));
- }
-
- private static String basicAuth(String userInfo) {
- if (userInfo != null)
- return "Basic "+new String(Base64.encodeBase64(userInfo.getBytes()));
- return null;
- }
-
/**
- * @param target
+ * @param base the base URL of Jenkins
+ * @param relativeTarget
* The endpoint that we are making requests to.
* @param authorization
* The value of the authorization header, if non-null.
*/
- public FullDuplexHttpStream(URL target, String authorization) throws IOException {
- this.target = target;
- this.authorization = authorization;
+ public FullDuplexHttpStream(URL base, String relativeTarget, String authorization) throws IOException {
+ if (!base.toString().endsWith("/")) {
+ throw new IllegalArgumentException(base.toString());
+ }
+ if (relativeTarget.startsWith("/")) {
+ throw new IllegalArgumentException(relativeTarget);
+ }
+
+ this.base = tryToResolveRedirects(base, authorization);
- CrumbData crumbData = new CrumbData();
+ URL target = new URL(this.base, relativeTarget);
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");
@@ -71,16 +68,16 @@ public class FullDuplexHttpStream {
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
- if(con.getHeaderField("Hudson-Duplex")==null)
- throw new IOException(target+" doesn't look like Jenkins");
+ // 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");
@@ -91,68 +88,29 @@ public class FullDuplexHttpStream {
if (authorization != null) {
con.addRequestProperty ("Authorization", authorization);
}
-
- if(crumbData.isValid) {
- con.addRequestProperty(crumbData.crumbName, crumbData.crumb);
- }
output = con.getOutputStream();
+ LOGGER.fine("established upload side");
}
- 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() {
- String url = target.toExternalForm();
- return new StringBuilder(url.substring(0, url.lastIndexOf("/cli"))).append("/crumbIssuer/api/xml/").toString();
- }
-
- private String readData(String dest) throws IOException {
- HttpURLConnection con = (HttpURLConnection) new URL(dest).openConnection();
+ // As this transport mode is using POST, it is necessary to resolve possible redirections using GET first.
+ private URL tryToResolveRedirects(URL base, String authorization) {
+ try {
+ HttpURLConnection con = (HttpURLConnection) base.openConnection();
if (authorization != null) {
con.addRequestProperty("Authorization", authorization);
}
- 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();
- }
+ con.getInputStream().close();
+ base = con.getURL();
+ } catch (Exception ex) {
+ // Do not obscure the problem propagating the exception. If the problem is real it will manifest during the
+ // actual exchange so will be reported properly there. If it is not real (no permission in UI but sufficient
+ // for CLI connection using one of its mechanisms), there is no reason to bother user about it.
+ LOGGER.log(Level.FINE, "Failed to resolve potential redirects", ex);
}
+ return base;
}
+
+ static final int BLOCK_SIZE = 1024;
+ static final Logger LOGGER = Logger.getLogger(FullDuplexHttpStream.class.getName());
+
}
diff --git a/cli/src/main/java/hudson/cli/HexDump.java b/cli/src/main/java/hudson/cli/HexDump.java
new file mode 100644
index 0000000000000000000000000000000000000000..ad37158bc16d0e7939a34831bd7628120eb61207
--- /dev/null
+++ b/cli/src/main/java/hudson/cli/HexDump.java
@@ -0,0 +1,47 @@
+package hudson.cli;
+
+// TODO COPIED FROM hudson.remoting
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+class HexDump {
+ private static final String CODE = "0123456789abcdef";
+
+ public static String toHex(byte[] buf) {
+ return toHex(buf,0,buf.length);
+ }
+ public static String toHex(byte[] buf, int start, int len) {
+ StringBuilder r = new StringBuilder(len*2);
+ boolean inText = false;
+ for (int i=0; i= 0x20 && b <= 0x7e) {
+ if (!inText) {
+ inText = true;
+ r.append('\'');
+ }
+ r.append((char) b);
+ } else {
+ if (inText) {
+ r.append("' ");
+ inText = false;
+ }
+ r.append("0x");
+ r.append(CODE.charAt((b>>4)&15));
+ r.append(CODE.charAt(b&15));
+ if (i < len - 1) {
+ if (b == 10) {
+ r.append('\n');
+ } else {
+ r.append(' ');
+ }
+ }
+ }
+ }
+ if (inText) {
+ r.append('\'');
+ }
+ return r.toString();
+ }
+}
diff --git a/cli/src/main/java/hudson/cli/PlainCLIProtocol.java b/cli/src/main/java/hudson/cli/PlainCLIProtocol.java
new file mode 100644
index 0000000000000000000000000000000000000000..ed5c453360dd2475ca7605adeee13476004b694e
--- /dev/null
+++ b/cli/src/main/java/hudson/cli/PlainCLIProtocol.java
@@ -0,0 +1,346 @@
+/*
+ * 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 hudson.cli;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.ReadPendingException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.input.CountingInputStream;
+
+/**
+ * CLI protocol working over a plain socket-like connection, without SSH or Remoting.
+ * Each side consists of frames starting with an {@code int} length,
+ * then a {@code byte} opcode, then any opcode-specific data.
+ * The length does not count the length field itself nor the opcode, so it is nonnegative.
+ */
+class PlainCLIProtocol {
+
+ static final Logger LOGGER = Logger.getLogger(PlainCLIProtocol.class.getName());
+
+ /** One-byte operation to send to the other side. */
+ private enum Op {
+ /** UTF-8 command name or argument. */
+ ARG(true),
+ /** UTF-8 locale identifier. */
+ LOCALE(true),
+ /** UTF-8 client encoding. */
+ ENCODING(true),
+ /** Start running command. */
+ START(true),
+ /** Exit code, as int. */
+ EXIT(false),
+ /** Chunk of stdin, as int length followed by bytes. */
+ STDIN(true),
+ /** EOF on stdin. */
+ END_STDIN(true),
+ /** Chunk of stdout. */
+ STDOUT(false),
+ /** Chunk of stderr. */
+ STDERR(false);
+ /** True if sent from the client to the server; false if sent from the server to the client. */
+ final boolean clientSide;
+ Op(boolean clientSide) {
+ this.clientSide = clientSide;
+ }
+ }
+
+ static abstract class EitherSide implements Closeable {
+
+ private final CountingInputStream cis;
+ private final FlightRecorderInputStream flightRecorder;
+ final DataInputStream dis;
+ final DataOutputStream dos;
+
+ protected EitherSide(InputStream is, OutputStream os) {
+ cis = new CountingInputStream(is);
+ flightRecorder = new FlightRecorderInputStream(cis);
+ dis = new DataInputStream(flightRecorder);
+ dos = new DataOutputStream(os);
+ }
+
+ final void begin() {
+ new Reader().start();
+ }
+
+ private class Reader extends Thread {
+
+ Reader() {
+ super("PlainCLIProtocol"); // TODO set distinctive Thread.name
+ }
+
+ @Override
+ public void run() {
+ try {
+ while (true) {
+ LOGGER.finest("reading frame");
+ int framelen;
+ try {
+ framelen = dis.readInt();
+ } catch (EOFException x) {
+ handleClose();
+ break; // TODO verify that we hit EOF immediately, not partway into framelen
+ }
+ if (framelen < 0) {
+ throw new IOException("corrupt stream: negative frame length");
+ }
+ byte b = dis.readByte();
+ if (b < 0) { // i.e., >127
+ throw new IOException("corrupt stream: negative operation code");
+ }
+ if (b >= Op.values().length) {
+ LOGGER.log(Level.WARNING, "unknown operation #{0}: {1}", new Object[] {b, HexDump.toHex(flightRecorder.getRecord())});
+ IOUtils.skipFully(dis, framelen);
+ continue;
+ }
+ Op op = Op.values()[b];
+ long start = cis.getByteCount();
+ LOGGER.log(Level.FINEST, "handling frame with {0} of length {1}", new Object[] {op, framelen});
+ boolean handled = handle(op, framelen);
+ if (handled) {
+ long actuallyRead = cis.getByteCount() - start;
+ if (actuallyRead != framelen) {
+ throw new IOException("corrupt stream: expected to read " + framelen + " bytes from " + op + " but read " + actuallyRead);
+ }
+ } else {
+ LOGGER.log(Level.WARNING, "unexpected {0}: {1}", new Object[] {op, HexDump.toHex(flightRecorder.getRecord())});
+ IOUtils.skipFully(dis, framelen);
+ }
+ }
+ } catch (ClosedChannelException x) {
+ LOGGER.log(Level.FINE, null, x);
+ handleClose();
+ } catch (IOException x) {
+ LOGGER.log(Level.WARNING, null, flightRecorder.analyzeCrash(x, "broken stream"));
+ } catch (ReadPendingException x) {
+ // in case trick in CLIAction does not work
+ LOGGER.log(Level.FINE, null, x);
+ handleClose();
+ } catch (RuntimeException x) {
+ LOGGER.log(Level.WARNING, null, x);
+ handleClose();
+ }
+ }
+
+ }
+
+ protected abstract void handleClose();
+
+ protected abstract boolean handle(Op op, int framelen) throws IOException;
+
+ private void writeOp(Op op) throws IOException {
+ dos.writeByte((byte) op.ordinal());
+ }
+
+ protected final synchronized void send(Op op) throws IOException {
+ dos.writeInt(0);
+ writeOp(op);
+ dos.flush();
+ }
+
+ protected final synchronized void send(Op op, int number) throws IOException {
+ dos.writeInt(4);
+ writeOp(op);
+ dos.writeInt(number);
+ dos.flush();
+ }
+
+ protected final synchronized void send(Op op, byte[] chunk, int off, int len) throws IOException {
+ dos.writeInt(len);
+ writeOp(op);
+ dos.write(chunk, off, len);
+ dos.flush();
+ }
+
+ protected final void send(Op op, byte[] chunk) throws IOException {
+ send(op, chunk, 0, chunk.length);
+ }
+
+ protected final void send(Op op, String text) throws IOException {
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ new DataOutputStream(buf).writeUTF(text);
+ send(op, buf.toByteArray());
+ }
+
+ protected final byte[] readChunk(int framelen) throws IOException {
+ assert Thread.currentThread() instanceof EitherSide.Reader;
+ byte[] buf = new byte[framelen];
+ dis.readFully(buf);
+ return buf;
+ }
+
+ protected final OutputStream stream(final Op op) {
+ return new OutputStream() {
+ @Override
+ public void write(int b) throws IOException {
+ send(op, new byte[] {(byte) b});
+ }
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ send(op, b, off, len);
+ }
+ @Override
+ public void write(byte[] b) throws IOException {
+ send(op, b);
+ }
+ };
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ dos.close();
+ }
+
+ }
+
+ static abstract class ServerSide extends EitherSide {
+
+ ServerSide(InputStream is, OutputStream os) {
+ super(is, os);
+ }
+
+ @Override
+ protected final boolean handle(Op op, int framelen) throws IOException {
+ assert Thread.currentThread() instanceof EitherSide.Reader;
+ assert op.clientSide;
+ switch (op) {
+ case ARG:
+ onArg(dis.readUTF());
+ return true;
+ case LOCALE:
+ onLocale(dis.readUTF());
+ return true;
+ case ENCODING:
+ onEncoding(dis.readUTF());
+ return true;
+ case START:
+ onStart();
+ return true;
+ case STDIN:
+ onStdin(readChunk(framelen));
+ return true;
+ case END_STDIN:
+ onEndStdin();
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ protected abstract void onArg(String text);
+
+ protected abstract void onLocale(String text);
+
+ protected abstract void onEncoding(String text);
+
+ protected abstract void onStart();
+
+ protected abstract void onStdin(byte[] chunk) throws IOException;
+
+ protected abstract void onEndStdin() throws IOException;
+
+ public final void sendExit(int code) throws IOException {
+ send(Op.EXIT, code);
+ }
+
+ public final OutputStream streamStdout() {
+ return stream(Op.STDOUT);
+ }
+
+ public final OutputStream streamStderr() {
+ return stream(Op.STDERR);
+ }
+
+ }
+
+ static abstract class ClientSide extends EitherSide {
+
+ ClientSide(InputStream is, OutputStream os) {
+ super(is, os);
+ }
+
+ @Override
+ protected boolean handle(Op op, int framelen) throws IOException {
+ assert Thread.currentThread() instanceof EitherSide.Reader;
+ assert !op.clientSide;
+ switch (op) {
+ case EXIT:
+ onExit(dis.readInt());
+ return true;
+ case STDOUT:
+ onStdout(readChunk(framelen));
+ return true;
+ case STDERR:
+ onStderr(readChunk(framelen));
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ protected abstract void onExit(int code);
+
+ protected abstract void onStdout(byte[] chunk) throws IOException;
+
+ protected abstract void onStderr(byte[] chunk) throws IOException;
+
+ public final void sendArg(String text) throws IOException {
+ send(Op.ARG, text);
+ }
+
+ public final void sendLocale(String text) throws IOException {
+ send(Op.LOCALE, text);
+ }
+
+ public final void sendEncoding(String text) throws IOException {
+ send(Op.ENCODING, text);
+ }
+
+ public final void sendStart() throws IOException {
+ send(Op.START);
+ }
+
+ public final OutputStream streamStdin() {
+ return stream(Op.STDIN);
+ }
+
+ public final void sendEndStdin() throws IOException {
+ send(Op.END_STDIN);
+ }
+
+ }
+
+ private PlainCLIProtocol() {}
+
+}
diff --git a/cli/src/main/java/hudson/cli/PrivateKeyProvider.java b/cli/src/main/java/hudson/cli/PrivateKeyProvider.java
index a1f6b3389030fe8e13a990cc5c0e3eabd5ad647f..bbf7873e2917f1b4edd456c8d53a23d48bf74459 100644
--- a/cli/src/main/java/hudson/cli/PrivateKeyProvider.java
+++ b/cli/src/main/java/hudson/cli/PrivateKeyProvider.java
@@ -24,25 +24,25 @@
package hudson.cli;
import static java.util.logging.Level.FINE;
+import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.Console;
import java.io.DataInputStream;
+import java.io.ByteArrayInputStream;
import java.io.File;
-import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
import java.security.GeneralSecurityException;
-import java.security.KeyFactory;
import java.security.KeyPair;
-import java.security.spec.DSAPrivateKeySpec;
-import java.security.spec.DSAPublicKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
-import com.trilead.ssh2.crypto.PEMDecoder;
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.util.security.SecurityUtils;
/**
* Read DSA or RSA key from file(s) asking for password interactively.
@@ -70,7 +70,7 @@ public class PrivateKeyProvider {
/**
* Read keys from default keyFiles
*
- * .ssh/id_rsa, .ssh/id_dsa and .ssh/identity.
+ * {@code .ssh/id_rsa}, {@code .ssh/id_dsa} and {@code .ssh/identity}.
*
* @return true if some key was read successfully.
*/
@@ -134,26 +134,15 @@ public class PrivateKeyProvider {
byte[] bytes = new byte[(int) f.length()];
dis.readFully(bytes);
return new String(bytes);
+ } catch (InvalidPathException e) {
+ throw new IOException(e);
}
}
public static KeyPair loadKey(String pemString, String passwd) throws IOException, GeneralSecurityException {
- Object key = PEMDecoder.decode(pemString.toCharArray(), passwd);
- if (key instanceof com.trilead.ssh2.signature.RSAPrivateKey) {
- com.trilead.ssh2.signature.RSAPrivateKey x = (com.trilead.ssh2.signature.RSAPrivateKey)key;
-
- return x.toJCEKeyPair();
- }
- if (key instanceof com.trilead.ssh2.signature.DSAPrivateKey) {
- com.trilead.ssh2.signature.DSAPrivateKey x = (com.trilead.ssh2.signature.DSAPrivateKey)key;
- KeyFactory kf = KeyFactory.getInstance("DSA");
-
- return new KeyPair(
- kf.generatePublic(new DSAPublicKeySpec(x.getY(), x.getP(), x.getQ(), x.getG())),
- kf.generatePrivate(new DSAPrivateKeySpec(x.getX(), x.getP(), x.getQ(), x.getG())));
- }
-
- throw new UnsupportedOperationException("Unrecognizable key format: " + key);
+ return SecurityUtils.loadKeyPairIdentity("key",
+ new ByteArrayInputStream(pemString.getBytes(UTF_8)),
+ FilePasswordProvider.of(passwd));
}
private static final Logger LOGGER = Logger.getLogger(PrivateKeyProvider.class.getName());
diff --git a/cli/src/main/java/hudson/cli/SSHCLI.java b/cli/src/main/java/hudson/cli/SSHCLI.java
new file mode 100644
index 0000000000000000000000000000000000000000..39dc9210a3e1615bac888932c689d97ad4dd1b4d
--- /dev/null
+++ b/cli/src/main/java/hudson/cli/SSHCLI.java
@@ -0,0 +1,136 @@
+/*
+ * 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 hudson.cli;
+
+import hudson.util.QuotedStringTokenizer;
+import java.io.IOException;
+import java.net.SocketAddress;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import static java.util.logging.Level.FINE;
+import java.util.logging.Logger;
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.channel.ClientChannel;
+import org.apache.sshd.client.channel.ClientChannelEvent;
+import org.apache.sshd.client.future.ConnectFuture;
+import org.apache.sshd.client.keyverifier.DefaultKnownHostsServerKeyVerifier;
+import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier;
+import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.future.WaitableFuture;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.io.NoCloseOutputStream;
+import org.apache.sshd.common.util.security.SecurityUtils;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * Implements SSH connection mode of {@link CLI}.
+ * In a separate class to avoid any class loading of {@code sshd-core} when not using {@code -ssh} mode.
+ * That allows the {@code test} module to pick up a version of {@code sshd-core} from the {@code sshd} module via {@code jenkins-war}
+ * that may not match the version being used from the {@code cli} module and may not be compatible.
+ */
+class SSHCLI {
+
+ @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE", justification = "Due to whatever reason FindBugs reports it fot try-with-resources")
+ static int sshConnection(String jenkinsUrl, String user, List args, PrivateKeyProvider provider, final boolean strictHostKey) throws IOException {
+ Logger.getLogger(SecurityUtils.class.getName()).setLevel(Level.WARNING); // suppress: BouncyCastle not registered, using the default JCE provider
+ URL url = new URL(jenkinsUrl + "login");
+ URLConnection conn = url.openConnection();
+ CLI.verifyJenkinsConnection(conn);
+ String endpointDescription = conn.getHeaderField("X-SSH-Endpoint");
+
+ if (endpointDescription == null) {
+ CLI.LOGGER.warning("No header 'X-SSH-Endpoint' returned by Jenkins");
+ return -1;
+ }
+
+ CLI.LOGGER.log(FINE, "Connecting via SSH to: {0}", endpointDescription);
+
+ int sshPort = Integer.parseInt(endpointDescription.split(":")[1]);
+ String sshHost = endpointDescription.split(":")[0];
+
+ StringBuilder command = new StringBuilder();
+
+ for (String arg : args) {
+ command.append(QuotedStringTokenizer.quote(arg));
+ command.append(' ');
+ }
+
+ try(SshClient client = SshClient.setUpDefaultClient()) {
+
+ KnownHostsServerKeyVerifier verifier = new DefaultKnownHostsServerKeyVerifier(new ServerKeyVerifier() {
+ @Override
+ public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) {
+ CLI.LOGGER.log(Level.WARNING, "Unknown host key for {0}", remoteAddress.toString());
+ return !strictHostKey;
+ }
+ }, true);
+
+ client.setServerKeyVerifier(verifier);
+ client.start();
+
+ ConnectFuture cf = client.connect(user, sshHost, sshPort);
+ cf.await();
+ try (ClientSession session = cf.getSession()) {
+ for (KeyPair pair : provider.getKeys()) {
+ CLI.LOGGER.log(FINE, "Offering {0} private key", pair.getPrivate().getAlgorithm());
+ session.addPublicKeyIdentity(pair);
+ }
+ session.auth().verify(10000L);
+
+ try (ClientChannel channel = session.createExecChannel(command.toString())) {
+ channel.setIn(new NoCloseInputStream(System.in));
+ channel.setOut(new NoCloseOutputStream(System.out));
+ channel.setErr(new NoCloseOutputStream(System.err));
+ WaitableFuture wf = channel.open();
+ wf.await();
+
+ Set waitMask = channel.waitFor(Collections.singletonList(ClientChannelEvent.CLOSED), 0L);
+
+ if(waitMask.contains(ClientChannelEvent.TIMEOUT)) {
+ throw new SocketTimeoutException("Failed to retrieve command result in time: " + command);
+ }
+
+ Integer exitStatus = channel.getExitStatus();
+ return exitStatus;
+
+ }
+ } finally {
+ client.stop();
+ }
+ }
+ }
+
+ private SSHCLI() {}
+
+}
diff --git a/cli/src/main/java/hudson/cli/SequenceOutputStream.java b/cli/src/main/java/hudson/cli/SequenceOutputStream.java
deleted file mode 100644
index fb6c26523b47317af89a975c7bd67930095ccabc..0000000000000000000000000000000000000000
--- a/cli/src/main/java/hudson/cli/SequenceOutputStream.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package hudson.cli;
-
-import java.io.OutputStream;
-import java.io.IOException;
-import java.io.SequenceInputStream;
-
-/**
- * {@link OutputStream} version of {@link SequenceInputStream}.
- *
- * Provides a single {@link OutputStream} view over multiple {@link OutputStream}s (each of the fixed length.)
- *
- * @author Kohsuke Kawaguchi
- */
-abstract class SequenceOutputStream extends OutputStream {
- protected static class Block {
- final OutputStream out;
- long capacity;
-
- public Block(OutputStream out, long capacity) {
- this.out = out;
- this.capacity = capacity;
- }
- }
-
- /**
- * Current block being written.
- */
- private Block block;
-
- protected SequenceOutputStream(Block block) {
- this.block = block;
- }
-
- @Override
- public void write(byte[] b, int off, int len) throws IOException {
- while(len>0) {
- int sz = (int)Math.min(len, block.capacity);
- block.out.write(b,off,sz);
- block.capacity -=sz;
- len-=sz;
- off+=sz;
- swapIfNeeded();
- }
- }
-
- public void write(int b) throws IOException {
- block.out.write(b);
- block.capacity--;
- swapIfNeeded();
- }
-
- private void swapIfNeeded() throws IOException {
- if(block.capacity >0) return;
- block.out.close();
- block=next(block);
- }
-
- @Override
- public void flush() throws IOException {
- block.out.flush();
- }
-
- @Override
- public void close() throws IOException {
- block.out.close();
- block=null;
- }
-
- /**
- * Fetches the next {@link OutputStream} to write to,
- * along with their capacity.
- */
- protected abstract Block next(Block current) throws IOException;
-}
diff --git a/core/src/main/java/hudson/util/QuotedStringTokenizer.java b/cli/src/main/java/hudson/util/QuotedStringTokenizer.java
similarity index 100%
rename from core/src/main/java/hudson/util/QuotedStringTokenizer.java
rename to cli/src/main/java/hudson/util/QuotedStringTokenizer.java
diff --git a/cli/src/main/resources/hudson/cli/client/Messages.properties b/cli/src/main/resources/hudson/cli/client/Messages.properties
index 1c091fbfb0d20ce33432ee40dd0f356c40f236c1..64a91d5ca56382027ae6b0259e00bf769d4a6223 100644
--- a/cli/src/main/resources/hudson/cli/client/Messages.properties
+++ b/cli/src/main/resources/hudson/cli/client/Messages.properties
@@ -1,15 +1,22 @@
CLI.Usage=Jenkins CLI\n\
Usage: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\
Options:\n\
- -s URL : the server URL (defaults to the JENKINS_URL env var)\n\
- -i KEY : SSH private key file used for authentication\n\
- -p HOST:PORT : HTTP proxy host and port for HTTPS proxy tunneling. See https://jenkins.io/redirect/cli-https-proxy-tunnel\n\
- -noCertificateCheck : bypass HTTPS certificate check entirely. Use with caution\n\
- -noKeyAuth : don't try to load the SSH authentication private key. Conflicts with -i\n\
- -v : verbose output. Display logs on the console by setting j.u.l log level to FINEST\n\
+ \ -s URL : the server URL (defaults to the JENKINS_URL env var)\n\
+ \ -http : use a plain CLI protocol over HTTP(S) (the default; mutually exclusive with -ssh)\n\
+ \ -ssh : use SSH protocol (requires -user; SSH port must be open on server, and user must have registered a public key)\n\
+ \ -i KEY : SSH private key file used for authentication (for use with -ssh)\n\
+ \ -noCertificateCheck : bypass HTTPS certificate check entirely. Use with caution\n\
+ \ -noKeyAuth : don''t try to load the SSH authentication private key. Conflicts with -i\n\
+ \ -user : specify user (for use with -ssh)\n\
+ \ -strictHostKey : request strict host key checking (for use with -ssh)\n\
+ \ -logger FINE : enable detailed logging from the client\n\
+ \ -auth [ USER:SECRET | @FILE ] : specify username and either password or API token (or load from them both from a file);\n\
+ \ for use with -http, or -remoting but only when the JNLP agent port is disabled.\n\
+ \ Passing credentials by file is recommended.\n\
+ \ See https://jenkins.io/redirect/cli-http-connection-mode for more info and options.\n\
\n\
- The available commands depend on the server. Run the 'help' command to\n\
+ The available commands depend on the server. Run the ''help'' command to \
see the list.
CLI.NoURL=Neither -s nor the JENKINS_URL env var is specified.
-CLI.VersionMismatch=Version mismatch. This CLI cannot work with this Jenkins server.
CLI.NoSuchFileExists=No such file exists: {0}
+CLI.BadAuth=The JENKINS_USER_ID and JENKINS_API_TOKEN env vars should be both set or left empty.
diff --git a/cli/src/main/resources/hudson/cli/client/Messages_es.properties b/cli/src/main/resources/hudson/cli/client/Messages_es.properties
index f9979a5619831289dfb4952ffe328e4d6d6b9798..b8ff42b3c23b707bdd8b105f53a624e034ebcc7d 100644
--- a/cli/src/main/resources/hudson/cli/client/Messages_es.properties
+++ b/cli/src/main/resources/hudson/cli/client/Messages_es.properties
@@ -1,10 +1,24 @@
CLI.Usage=Jenkins CLI\n\
- Usar: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\
- Options:\n\
- \ -s URL : direccin web (por defecto se usa la variable JENKINS_URL)\n\
+ Uso: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\
+ Opciones:\n\
+ -s URL : direcci\u00f3n web (por defecto se usa la variable JENKINS_URL)\n\
+ -http : usa un protocolo plano sobre HTTP(S) (es el uso por defecto; excluyente con -ssh y -remoting)\n\
+ -ssh : usa el protocolo SSH (requiere -user; el puerto SSH debe estar abierto en el servidor y el usuario debe tener registrada su clave publica)\n\
+ -remoting : usa el protocolo deprecado de Remoting (siempre que est\u00e9 habilitado en el servidor; s\u00f3lo para compatibilidad con comandos heredados o legacy)\n\
+ -i KEY : clave privada SSH usada para autenticaci\u00f3n (usado con -ssh o -remoting)\n\
+ -p HOST:PORT : host y puerto para el uso de proxy HTTPS. Ver https://jenkins.io/redirect/cli-https-proxy-tunnel\n\
+ -noCertificateCheck : elude por completo la verificaci\u00f3n del certificado HTTPS. Usar con precauci\u00f3n\n\
+ -noKeyAuth : intenta no cargar la clave privada de autenticaci\u00f3n SSH. Presenta conflicto con -i\n\
+ -user : especifica el usuario (se usa con -ssh)\n\
+ -strictHostKey : solicita la comprobaci\u00f3n estricta de la clave del host (se usa con -ssh)\n\
+ -logger FINE : habilita logs detallados en el cliente\n\
+ -auth [ USER:SECRET | @FILE ] : especifica el usuario y contrase\u00f1a o API token (o los carga de un fichero);\n\
+ se usa con -http o -remoting pero s\u00f3lo si el puerto del agente JNLP est\u00e1 deshabilitado.\n\
+ No es necesario si las variables de entorno JENKINS_USER_ID y JENKINS_API_TOKEN se encuentran configuradas.\n\
\n\
- La lista completa de comandos disponibles depende del servidor. Ejecuta\n\
- el comando ''help'' para ver la lista.
-CLI.NoURL=No se ha especificado el parmetro -s ni la variable JENKINS_URL
-CLI.VersionMismatch=La versin no coincide. Esta consola "CLI" no se puede usar en este Jenkins
+ La lista de comandos disponibles depende del servidor. Ejecuta\n\
+ el comando ''help'' para ver la lista completa.
+CLI.NoURL=No se ha especificado el par\u00e1metro -s ni la variable JENKINS_URL
+CLI.VersionMismatch=La versi\u00f3n no coincide. Esta consola "CLI" no se puede usar en este Jenkins
CLI.NoSuchFileExists=El fichero {0} no existe.
+CLI.BadAuth=Ambas variables de entorno JENKINS_USER_ID y JENKINS_API_TOKEN deber\u00edan estar configuradas o mantenerse vac\u00edas.
diff --git a/cli/src/main/resources/hudson/cli/client/Messages_it.properties b/cli/src/main/resources/hudson/cli/client/Messages_it.properties
new file mode 100644
index 0000000000000000000000000000000000000000..6236107d9c7cff3786ea619aba6097291835d6f7
--- /dev/null
+++ b/cli/src/main/resources/hudson/cli/client/Messages_it.properties
@@ -0,0 +1,22 @@
+CLI.NoSuchFileExists=File non esistente: {0}
+CLI.NoURL=Non sono stati specificati n -s n la variabile d''ambiente JENKINS_URL.
+CLI.VersionMismatch=Le versioni non corrispondono. Quest''interfaccia della riga di comando non pu funzionare con questo server Jenkins.
+CLI.Usage=Interfaccia della riga di comando di Jenkins\n\
+Uso: java -jar jenkins-cli.jar [-s URL] comando [opzioni...] argomenti...\n\
+Opzioni:\n\
+-s URL : l''URL del server (impostazione predefinita: la variabile d''ambiente JENKINS_URL)\n\
+-http : utilizza un protocollo interfaccia della riga di comando in testo semplice su HTTP(S) (impostazione predefinita; mutualmente esclusiva con -ssh e -remoting)\n\
+-ssh : utilizza il protocollo SSH (richiede -user; la porta SSH deve essere aperta sul server e l''utente deve aver registrato una chiave pubblica)\n\
+-remoting : utilizza il protocollo deprecato Remoting channel (se abilitato sul server; solo per compatibilit con comandi o modalit comandi legacy)\n\
+-i KEY : file chiave privata SSH utilizzato per l''autenticazione (per l''utilizzo con -ssh o -remoting)\n\
+-p HOST:PORTA : host proxy HTTP e porta per il tunneling proxy HTTPS. Vedi https://jenkins.io/redirect/cli-https-proxy-tunnel\n\
+-noCertificateCheck : ometti completamente il controllo dei certificati HTTPS. Utilizzare con cautela\n\
+-noKeyAuth : non tentare di caricare la chiave privata per l''autenticazione SSH. In conflitto con -i\n\
+-user : specifica l''utente (per l''utilizzo con -ssh)\n\
+-strictHostKey : richiedi la modalit strict per il controllo delle chiavi host (per l''utilizzo con -ssh)\n\
+-logger FINE : abilita la registrazione dettagliata da parte del client\n\
+-auth [ UTENTE:SEGRETO | @FILE ] : specifica il nome utente e la password o il token API (o caricali entrambi da un file);\n\
+per l''utilizzo con -http, o -remoting ma solo quando la porta agente JNLP disabilitata\n\
+\n\
+I comandi disponibili dipendono dal server. Eseguire il comando ''help'' per\n\
+visualizzarne l''elenco.
diff --git a/cli/src/main/resources/hudson/cli/client/Messages_zh_CN.properties b/cli/src/main/resources/hudson/cli/client/Messages_zh_CN.properties
new file mode 100644
index 0000000000000000000000000000000000000000..16fa41c48c9a4aad3d63e9f40e2d017b3bd9fb77
--- /dev/null
+++ b/cli/src/main/resources/hudson/cli/client/Messages_zh_CN.properties
@@ -0,0 +1,44 @@
+# The MIT License
+#
+# Copyright (c) 2013, Sun Microsystems, Inc., Kohsuke Kawaguchi, Pei-Tang Huang,
+# Chunghwa Telecom Co., Ltd., and a number of other of contributers
+#
+# 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.
+
+CLI.Usage=Jenkins CLI\n\
+ \u7528\u6cd5: java -jar jenkins-cli.jar [-s url] \u547d\u4ee4 [\u9009\u9879...] \u53c2\u6570...\n\
+ \u9009\u9879:\n\
+ -s URL : \u670d\u52a1\u5668url \uff08\u9ed8\u8ba4\u662fjenkins_url\u73af\u5883\u53d8\u91cf\uff09\n\
+ -http : \u5728http%28s%29\u4e0a\u4f7f\u7528\u539f\u59cb\u7684cli\u534f\u8bae\uff08\u9ed8\u8ba4\u503c%3b \u4e0e -ssh \u548c -remoting \u4e92\u65a5\uff09\n\
+ -ssh : \u4f7f\u7528ssh\u534f\u8bae\uff08\u9700\u8981 -user%3b \u670d\u52a1\u5668\u7684ssh\u7aef\u53e3\u5fc5\u987b\u6253\u5f00\uff0c\u4e14\u7528\u6237\u5fc5\u987b\u5df2\u6ce8\u518c\u516c\u94a5\u3002\uff09\n\
+ -remoting : \u4f7f\u7528\u4e0d\u63a8\u8350\u7684\u8fdc\u7a0b\u4fe1\u9053\u534f\u8bae \uff08\u5982\u679c\u670d\u52a1\u5668\u4e0a\u662f\u6253\u5f00\u7684\uff1b\u4ec5\u7528\u4e8e\u517c\u5bb9\u9057\u7559\u547d\u4ee4\u6216\u547d\u4ee4\u6a21\u5f0f\uff09\n\
+ -i KEY : \u7528\u4e8e\u8ba4\u8bc1\u7684ssh\u79c1\u94a5\u6587\u4ef6\uff08\u4e0e -ssh \u6216 -remoting \u4e00\u8d77\u4f7f\u7528\uff09\n\
+ -p HOST:PORT : \u7528\u6237https\u4ee3\u7406\u96a7\u9053\u7684http\u4ee3\u7406\u4e3b\u673a\u548c\u7aef\u53e3\u3002\u53c2\u89c1 https://jenkins.io/redirect/cli-https-proxy-tunnel\n\
+ -noCertificateCheck : \u5b8c\u5168\u5ffd\u7565https\u8bc1\u4e66\u8ba4\u8bc1\u3002\u8c28\u614e\u4f7f\u7528\n\
+ -noKeyAuth : \u65e0\u9700\u5c1d\u8bd5\u52a0\u8f7dssh\u8ba4\u8bc1\u79c1\u94a5\u3002\u4e0e -i \u76f8\u53cd\n\
+ -user : \u6307\u5b9a\u7528\u6237\uff08\u4e0e -ssh \u4e00\u8d77\u4f7f\u7528\uff09\n\
+ -strictHostKey : \u8981\u6c42\u9a8c\u8bc1\u4e3b\u673akey\u68c0\u67e5\uff08\u4e0e -ssh \u4e00\u8d77\u4f7f\u7528\uff09\n\
+ -logger FINE : \u5141\u8bb8\u5ba2\u6237\u7aef\u8be6\u7ec6\u65e5\u5fd7\u8bb0\u5f55\n\
+ -auth [ USER:SECRET | @FILE ] : \u6307\u5b9a\u7528\u6237\u540d\u4e0e\u5bc6\u7801\u6216\u7528\u6237\u540d\u4e0eapi token\uff08\u6216\u8005\u4ece\u6587\u4ef6\u52a0\u8f7d\uff09\uff1b\n\
+ \u4e0e -http \u4e00\u8d77\u4f7f\u7528\uff0c\u6216\u8005\u53ea\u5728jnlp\u4ee3\u7406\u7aef\u53e3\u7981\u7528\u65f6\u4e0e -remoting \u4e00\u8d77\u4f7f\u7528\n\
+ \n\
+ \u53ef\u7528\u7684\u547d\u4ee4\u53d6\u51b3\u4e8e\u670d\u52a1\u5668\u3002\u6267\u884c 'help' \u547d\u4ee4\u53ef\u4ee5\u67e5\u770b\u5b8c\u6574\u6e05\u5355\u3002
+CLI.NoURL=\u6c92\u6709\u6307\u5b9a -s \u53c2\u6570\u6216\u8005 jenkins_url \u73af\u5883\u53d8\u91cf\u3002
+CLI.VersionMismatch=\u7248\u672c\u4e0d\u5339\u914d\u3002cli \u65e0\u6cd5\u5728\u6b64 jenkins \u670d\u52a1\u5668\u4e0a\u8fd0\u884c\u3002
+CLI.NoSuchFileExists=\u6587\u4ef6\u4e0d\u5b58\u5728: {0}
diff --git a/cli/src/test/java/hudson/cli/ConnectionTest.java b/cli/src/test/java/hudson/cli/ConnectionTest.java
deleted file mode 100644
index f4cb0e8b1d595516130553bbe6568f8255324327..0000000000000000000000000000000000000000
--- a/cli/src/test/java/hudson/cli/ConnectionTest.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package hudson.cli;
-
-import static org.junit.Assert.*;
-
-import hudson.remoting.FastPipedInputStream;
-import hudson.remoting.FastPipedOutputStream;
-import org.codehaus.groovy.runtime.Security218;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-
-import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
-import java.io.IOException;
-
-/**
- * @author Kohsuke Kawaguchi
- */
-public class ConnectionTest {
-
- Throwable e;
- private Connection c1;
- private Connection c2;
-
- @Before
- public void setUp() throws IOException {
- FastPipedInputStream i = new FastPipedInputStream();
- FastPipedInputStream j = new FastPipedInputStream();
-
- c1 = new Connection(i,new FastPipedOutputStream(j));
- c2 = new Connection(j,new FastPipedOutputStream(i));
- }
-
- @Test
- public void testEncrypt() throws Throwable {
- final SecretKey sessionKey = new SecretKeySpec(new byte[16],"AES");
-
- Thread t1 = new Thread() {
- @Override
- public void run() {
- try {
- c1.encryptConnection(sessionKey,"AES/CFB8/NoPadding").writeUTF("Hello");
- } catch (Throwable x) {
- e = x;
- }
- }
- };
- t1.start();
-
- Thread t2 = new Thread() {
- @Override
- public void run() {
- try {
- String data = c2.encryptConnection(sessionKey,"AES/CFB8/NoPadding").readUTF();
- assertEquals("Hello", data);
- } catch (Throwable x) {
- e = x;
- }
- }
- };
- t2.start();
-
- t1.join(9999);
- t2.join(9999);
-
- if (e != null) {
- throw e;
- }
-
- if (t1.isAlive() || t2.isAlive()) {
- t1.interrupt();
- t2.interrupt();
- throw new Error("thread is still alive");
- }
- }
-
- @Test
- public void testSecurity218() throws Exception {
- c1.writeObject(new Security218());
- try {
- c2.readObject();
- fail();
- } catch (SecurityException e) {
- assertTrue(e.getMessage().contains(Security218.class.getName()));
- }
- }
-}
diff --git a/cli/src/test/java/hudson/cli/PlainCLIProtocolTest.java b/cli/src/test/java/hudson/cli/PlainCLIProtocolTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4663fbafd5e0aa1a05852562ef8c8560f3d0cac3
--- /dev/null
+++ b/cli/src/test/java/hudson/cli/PlainCLIProtocolTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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 hudson.cli;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import static org.junit.Assert.*;
+import org.junit.Test;
+
+public class PlainCLIProtocolTest {
+
+ @Test
+ public void ignoreUnknownOperations() throws Exception {
+ final PipedOutputStream upload = new PipedOutputStream();
+ final PipedOutputStream download = new PipedOutputStream();
+ class Client extends PlainCLIProtocol.ClientSide {
+ int code = -1;
+ final ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+ Client() throws IOException {
+ super(new PipedInputStream(download), upload);
+ }
+ @Override
+ protected synchronized void onExit(int code) {
+ this.code = code;
+ notifyAll();
+ }
+ @Override
+ protected void onStdout(byte[] chunk) throws IOException {
+ stdout.write(chunk);
+ }
+ @Override
+ protected void onStderr(byte[] chunk) throws IOException {}
+ @Override
+ protected void handleClose() {}
+ void send() throws IOException {
+ sendArg("command");
+ sendStart();
+ streamStdin().write("hello".getBytes());
+ }
+ void newop() throws IOException {
+ dos.writeInt(0);
+ dos.writeByte(99);
+ dos.flush();
+ }
+ }
+ class Server extends PlainCLIProtocol.ServerSide {
+ String arg;
+ boolean started;
+ final ByteArrayOutputStream stdin = new ByteArrayOutputStream();
+ Server() throws IOException {
+ super(new PipedInputStream(upload), download);
+ }
+ @Override
+ protected void onArg(String text) {
+ arg = text;
+ }
+ @Override
+ protected void onLocale(String text) {}
+ @Override
+ protected void onEncoding(String text) {}
+ @Override
+ protected synchronized void onStart() {
+ started = true;
+ notifyAll();
+ }
+ @Override
+ protected void onStdin(byte[] chunk) throws IOException {
+ stdin.write(chunk);
+ }
+ @Override
+ protected void onEndStdin() throws IOException {}
+ @Override
+ protected void handleClose() {}
+ void send() throws IOException {
+ streamStdout().write("goodbye".getBytes());
+ sendExit(2);
+ }
+ void newop() throws IOException {
+ dos.writeInt(0);
+ dos.writeByte(99);
+ dos.flush();
+ }
+ }
+ Client client = new Client();
+ Server server = new Server();
+ client.begin();
+ server.begin();
+ client.send();
+ client.newop();
+ synchronized (server) {
+ while (!server.started) {
+ server.wait();
+ }
+ }
+ server.newop();
+ server.send();
+ synchronized (client) {
+ while (client.code == -1) {
+ client.wait();
+ }
+ }
+ assertEquals("hello", server.stdin.toString());
+ assertEquals("command", server.arg);
+ assertEquals("goodbye", client.stdout.toString());
+ assertEquals(2, client.code);
+ }
+
+}
diff --git a/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java b/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java
index 376b1fa815f3f62988f741c93a63f2f2d090563a..1bb49866ba6140ff1f8e27e905dfe4cd6e972862 100644
--- a/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java
+++ b/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java
@@ -1,140 +1,104 @@
-/*
- * The MIT License
- *
- * Copyright (c) 2014 Red Hat, 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 hudson.cli;
-import static org.mockito.Mockito.verify;
-import static org.powermock.api.mockito.PowerMockito.doReturn;
-import static org.powermock.api.mockito.PowerMockito.mock;
-import static org.powermock.api.mockito.PowerMockito.mockStatic;
-import static org.powermock.api.mockito.PowerMockito.whenNew;
+import org.junit.Test;
import java.io.File;
import java.io.IOException;
-import java.net.URISyntaxException;
-import java.net.URL;
+import java.lang.IllegalArgumentException;
import java.security.GeneralSecurityException;
-import java.security.Key;
import java.security.KeyPair;
-import java.util.Arrays;
+import java.security.NoSuchAlgorithmException;
-import org.hamcrest.Description;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentMatcher;
-import org.mockito.Mockito;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
+import static org.junit.Assert.assertNotNull;
-@RunWith(PowerMockRunner.class)
-@PrepareForTest(CLI.class) // When mocking new operator caller has to be @PreparedForTest, not class itself
+/**
+keys were generated with ssh-keygen from OpenSSH_7.9p1, LibreSSL 2.7.3
+*/
public class PrivateKeyProviderTest {
+ /**
+ key command: ssh-keygen -f dsa -t dsa -b 1024 -m PEM
+ */
@Test
- public void specifyKeysExplicitly() throws Exception {
- final CLI cli = fakeCLI();
-
- final File dsaKey = keyFile(".ssh/id_dsa");
- final File rsaKey = keyFile(".ssh/id_rsa");
-
- run("-i", dsaKey.getAbsolutePath(), "-i", rsaKey.getAbsolutePath(), "-s", "http://example.com");
-
- verify(cli).authenticate(withKeyPairs(
- keyPair(dsaKey),
- keyPair(rsaKey)
- ));
+ public void loadKeyDSA() throws IOException, GeneralSecurityException {
+ File file = new File(this.getClass().getResource("dsa").getFile());
+ String password = null;
+ KeyPair keyPair = PrivateKeyProvider.loadKey(file, password);
+ assertNotNull(keyPair);
+ assertNotNull(keyPair.getPrivate());
+ assertNotNull(keyPair.getPublic());
}
+ /**
+ key command: ssh-keygen -f dsa-password -t dsa -b 1024 -m PEM -p password
+ */
@Test
- public void useDefaultKeyLocations() throws Exception {
- final CLI cli = fakeCLI();
-
- final File rsaKey = keyFile(".ssh/id_rsa");
- final File dsaKey = keyFile(".ssh/id_dsa");
-
- fakeHome();
- run("-s", "http://example.com");
-
- verify(cli).authenticate(withKeyPairs(
- keyPair(rsaKey),
- keyPair(dsaKey)
- ));
+ public void loadKeyDSAPassword() throws IOException, GeneralSecurityException {
+ File file = new File(this.getClass().getResource("dsa-password").getFile());
+ String password = "password";
+ KeyPair keyPair = PrivateKeyProvider.loadKey(file, password);
+ assertNotNull(keyPair);
+ assertNotNull(keyPair.getPrivate());
+ assertNotNull(keyPair.getPublic());
}
-
- private CLI fakeCLI() throws Exception {
- final CLI cli = mock(CLI.class);
-
- final CLIConnectionFactory factory = mock(CLIConnectionFactory.class, Mockito.CALLS_REAL_METHODS);
- factory.jenkins = new URL("http://example.com");
- doReturn(cli).when(factory).connect();
-
- mockStatic(CLIConnectionFactory.class);
- whenNew(CLIConnectionFactory.class).withNoArguments().thenReturn(factory);
-
- return cli;
+
+ /**
+ key command: ssh-keygen -f rsa -t rsa -b 1024 -m PEM
+ */
+ @Test
+ public void loadKeyRSA() throws IOException, GeneralSecurityException {
+ File file = new File(this.getClass().getResource("rsa").getFile());
+ String password = null;
+ KeyPair keyPair = PrivateKeyProvider.loadKey(file, password);
+ assertNotNull(keyPair);
+ assertNotNull(keyPair.getPrivate());
+ assertNotNull(keyPair.getPublic());
}
- private void fakeHome() throws URISyntaxException {
- final File home = new File(this.getClass().getResource(".ssh").toURI()).getParentFile();
- System.setProperty("user.home", home.getAbsolutePath());
+ /**
+ key command: ssh-keygen -f rsa-password -t rsa -b 1024 -m PEM -p password
+ */
+ @Test
+ public void loadKeyRSAPassword() throws IOException, GeneralSecurityException {
+ File file = new File(this.getClass().getResource("rsa-password").getFile());
+ String password = "password";
+ KeyPair keyPair = PrivateKeyProvider.loadKey(file, password);
+ assertNotNull(keyPair);
+ assertNotNull(keyPair.getPrivate());
+ assertNotNull(keyPair.getPublic());
}
-
- private int run(String... args) throws Exception {
- return CLI._main(args);
+
+ /**
+ key command: ssh-keygen -f openssh -t rsa -b 1024
+ */
+ @Test
+ public void loadKeyOpenSSH() throws IOException, GeneralSecurityException {
+ File file = new File(this.getClass().getResource("openssh").getFile());
+ String password = null;
+ KeyPair keyPair = PrivateKeyProvider.loadKey(file, password);
+ assertNotNull(keyPair);
+ assertNotNull(keyPair.getPrivate());
+ assertNotNull(keyPair.getPublic());
}
-
- private File keyFile(String name) throws URISyntaxException {
- return new File(this.getClass().getResource(name).toURI());
+
+ /**
+ key command: ssh-keygen -f openssh-unsupported -t rsa -b 1024 -p password
+ */
+ @Test(expected = NoSuchAlgorithmException.class)
+ public void loadKeyUnsupportedCipher() throws IOException, GeneralSecurityException {
+ File file = new File(this.getClass().getResource("openssh-unsuported").getFile());
+ String password = "password";
+ PrivateKeyProvider.loadKey(file, password);
}
- private KeyPair keyPair(File file) throws IOException, GeneralSecurityException {
- return PrivateKeyProvider.loadKey(file, null);
- }
-
- private Iterable withKeyPairs(final KeyPair... expected) {
- return Mockito.argThat(new ArgumentMatcher>() {
- @Override public void describeTo(Description description) {
- description.appendText(Arrays.asList(expected).toString());
- }
-
- @Override public boolean matches(Object argument) {
- if (!(argument instanceof Iterable)) throw new IllegalArgumentException("Not an instance of Iterrable");
-
- @SuppressWarnings("unchecked")
- final Iterable actual = (Iterable) argument;
- int i = 0;
- for (KeyPair akp: actual) {
- if (!eq(expected[i].getPublic(), akp.getPublic())) return false;
- if (!eq(expected[i].getPrivate(), akp.getPrivate())) return false;
- i++;
- }
-
- return i == expected.length;
- }
-
- private boolean eq(final Key expected, final Key actual) {
- return Arrays.equals(expected.getEncoded(), actual.getEncoded());
- }
- });
+ /**
+ key command: ssh-keygen -f openssh -t rsa -b 1024
+ in this key we remove some lines to break the key.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void loadKeyBroken() throws IOException, GeneralSecurityException {
+ File file = new File(this.getClass().getResource("openssh-broken").getFile());
+ String password = "password";
+ PrivateKeyProvider.loadKey(file, password);
}
}
diff --git a/cli/src/test/java/org/codehaus/groovy/runtime/Security218.java b/cli/src/test/java/org/codehaus/groovy/runtime/Security218.java
deleted file mode 100644
index cc3dfeef041c6ac07b91f80d3dc98a5f1530e4c7..0000000000000000000000000000000000000000
--- a/cli/src/test/java/org/codehaus/groovy/runtime/Security218.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.codehaus.groovy.runtime;
-
-import java.io.Serializable;
-
-/**
- * Test payload in a prohibited package name.
- *
- * @author Kohsuke Kawaguchi
- */
-public class Security218 implements Serializable {
-}
diff --git a/cli/src/test/resources/hudson/cli/.ssh/id_dsa b/cli/src/test/resources/hudson/cli/.ssh/id_dsa
deleted file mode 100644
index be556daa6d02984101686a02939b6777c780db86..0000000000000000000000000000000000000000
--- a/cli/src/test/resources/hudson/cli/.ssh/id_dsa
+++ /dev/null
@@ -1,12 +0,0 @@
------BEGIN DSA PRIVATE KEY-----
-MIIBugIBAAKBgQCA9mMzB1O52hpObIyaJXgFJQUmc1HV0NEJXsFFGh8U2l0Tkgv4
-fp3MWadiAMmc5H1ot4KQLXl7SwU7dHCCFcGcfQiOjeD5rWeZuHoPAJSDMilcJGE3
-Xo2C+wlescTByEgRRA16vdSlNaDJXKVxq9Wr59G8P4JC6/5EvpeypgYdTQIVAMTf
-aC0O2EGLnJrNBsUdc1s+iUp9AoGAZA7pZYPMJHJWTanJb2DlWHn/QM63jfh38N6W
-ERzmQQks6QdS7UkFlg9cbVGUtn0Yz2SfX3VKiMXNMkAdGD8loBcJS5w6oMMU7rcj
-lldRQ63+fMgdVZYMF5bchC6RhQeGZQ8Imf2iFF28SsE4bi+K12HYgIO5bFxPFUTH
-WSWsMLcCgYBgHJ90ZLU400axB5P0qw/0s4arPD0g53Vzi/Y2h5TJr3KPF2sEIbAc
-2gpFEzUNY0hvH6REKJ+VPPUvlH6ieaXomW8pSGjv4SdxZhJRrDe+Ac/xQse1QdYx
-uWJzpVm3cIGfqLxmQnrklnutI/1F62VZQlq9vjiZL7ir/00vdUTYHwIUUkttGGgl
-a0rWLzPTPF4X4lZfFhk=
------END DSA PRIVATE KEY-----
diff --git a/cli/src/test/resources/hudson/cli/.ssh/id_dsa.pub b/cli/src/test/resources/hudson/cli/.ssh/id_dsa.pub
deleted file mode 100644
index 4a42a845727297abcfa1c1204d0a270392c5a9a6..0000000000000000000000000000000000000000
--- a/cli/src/test/resources/hudson/cli/.ssh/id_dsa.pub
+++ /dev/null
@@ -1 +0,0 @@
-ssh-dss AAAAB3NzaC1kc3MAAACBAID2YzMHU7naGk5sjJoleAUlBSZzUdXQ0QlewUUaHxTaXROSC/h+ncxZp2IAyZzkfWi3gpAteXtLBTt0cIIVwZx9CI6N4PmtZ5m4eg8AlIMyKVwkYTdejYL7CV6xxMHISBFEDXq91KU1oMlcpXGr1avn0bw/gkLr/kS+l7KmBh1NAAAAFQDE32gtDthBi5yazQbFHXNbPolKfQAAAIBkDullg8wkclZNqclvYOVYef9AzreN+Hfw3pYRHOZBCSzpB1LtSQWWD1xtUZS2fRjPZJ9fdUqIxc0yQB0YPyWgFwlLnDqgwxTutyOWV1FDrf58yB1VlgwXltyELpGFB4ZlDwiZ/aIUXbxKwThuL4rXYdiAg7lsXE8VRMdZJawwtwAAAIBgHJ90ZLU400axB5P0qw/0s4arPD0g53Vzi/Y2h5TJr3KPF2sEIbAc2gpFEzUNY0hvH6REKJ+VPPUvlH6ieaXomW8pSGjv4SdxZhJRrDe+Ac/xQse1QdYxuWJzpVm3cIGfqLxmQnrklnutI/1F62VZQlq9vjiZL7ir/00vdUTYHw== ogondza@localhost.localdomain
diff --git a/cli/src/test/resources/hudson/cli/dsa b/cli/src/test/resources/hudson/cli/dsa
new file mode 100644
index 0000000000000000000000000000000000000000..4f3f64f95d0e0f1ce121fe33a5a00d25361991f5
--- /dev/null
+++ b/cli/src/test/resources/hudson/cli/dsa
@@ -0,0 +1,12 @@
+-----BEGIN DSA PRIVATE KEY-----
+MIIBvAIBAAKBgQCUlgM7pckNS/AU9w80QDlw304vdyK6u6cWXW7F1PFYMhCrsC7C
+ZcbSKo2mTPHk6P77z3zceSAHeYxkXx34N2HYWCth+79N3VIz/EZC/IaN0sea1teF
+XYLvdnDWazDcRdq+3d4iLAL+46QX6tIaEmUh9SFd4kz7P0rwPHz7SP7+MwIVAPnJ
+QmOwaMbyvb0Q0/OjtXF1orn5AoGBAIP6gOIhNErjgnyzHSfAaR/F2wNFW/TR5HFH
+GEnsVuxTXE00mjqZJaDznviDoVtWF/HlKnR6OcnVr7fm61PXKogeLeJ/zgNPWYCe
+7b45iO38Ztnmimmo9GbzTcDYOVDhNhgZEOUWes9Cdrku8yB3Ugf6nN6GY17LoQ9s
+EDk1Fa8TAoGAD3cFbpbnnw0sHaKMRopCPJKgmCtvzJFgf+xGUZRgEPbeB+1UnG2Z
+T8xPyPF4Zwyo0Mf/b0gxjynLpvzLrr3TMQjqjL4vGE1UIx638ff1Jw08tRRCfBIr
+GpIA5AIzZhxRE6aZREN5wqvnWnlEN+s6mOhcQ8gtNOObHaKuGdU+jAwCFQCvo83Q
+shI+la9gd69CWzJx5GJkJw==
+-----END DSA PRIVATE KEY-----
diff --git a/cli/src/test/resources/hudson/cli/dsa-password b/cli/src/test/resources/hudson/cli/dsa-password
new file mode 100644
index 0000000000000000000000000000000000000000..8a28798a8c0d1b8f37ee9d1b15d02c0c311efa44
--- /dev/null
+++ b/cli/src/test/resources/hudson/cli/dsa-password
@@ -0,0 +1,15 @@
+-----BEGIN DSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,CC014AB7E7C376D246B02422C40ED26A
+
+VjIeNQw7EVgiMDJVglpma1CIvRlfzhBBR6r0i5QsVEPTNGLNNUvbqMxUATud8ql0
+Dc84nDj47qbp+jfsvtSfau32hONynT91mNZiCTPUMGFQqiBDMXUMYBv64NXNFi51
++QpI+bW85KLJyRZSmxmBHH31s+6buWwzpYQ2ImwA/Zzkn/1+Evts+VYWEAwXZNBR
+rlpouBiY7XIP3hifU6vBwkH1Dr5Qff0hq13b4T74X2cL2cmWwmPjWDm8nXgDW2Of
+B+5+w2vCp/QDBUpc8LsxNqUK2/B7KA22wqpmQhD7dY4orzaHQWGKwojSd4LwfX6r
+3NEeptKAAn4TusyREypO67g7xUDT0BfZmCNUg6J9dLPWGusx8IvpK/hABzo5Etn/
+CnBRMc1QaDUASHRgzCkadM1DpKmyVQFlMLXCRJaS+51CHGVVGFILcgPsCcrzFL0r
+HOzDbBymhU9Mfw1AygdKNMyZ8NdDsdZ1H3fkkZu7sMgt3PRaYZG+wNImpkdnQDoN
+V3VqTZ5orF4Lo4OqxOuEd7dakpdpyidRvQQc5GaOZuG+2X/NlKhzdGEjt0iubMnX
+S94J+KPJUeVfPphtA2Lb8w==
+-----END DSA PRIVATE KEY-----
diff --git a/cli/src/test/resources/hudson/cli/openssh b/cli/src/test/resources/hudson/cli/openssh
new file mode 100644
index 0000000000000000000000000000000000000000..6d5ce34bdaa8dc9f3e87e499171327ce96938486
--- /dev/null
+++ b/cli/src/test/resources/hudson/cli/openssh
@@ -0,0 +1,17 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAIEA6YOQ5he3DIIgu+O9HJLvcGlLNbKe04rN7KftKhaALsqJAtS435bk
+xjv/ycn2uMnTapx2Q+Eu/wqATITB+SZEfAkgRMZmTa2Ze3zt/b6rieRhgRgovu3VyXsRnM
+fgCEuJqU5VRW5WlayYRUsJnQTSaeUuJJvQWAeo9TI/DtYzvp8AAAIYX2d44V9neOEAAAAH
+c3NoLXJzYQAAAIEA6YOQ5he3DIIgu+O9HJLvcGlLNbKe04rN7KftKhaALsqJAtS435bkxj
+v/ycn2uMnTapx2Q+Eu/wqATITB+SZEfAkgRMZmTa2Ze3zt/b6rieRhgRgovu3VyXsRnMfg
+CEuJqU5VRW5WlayYRUsJnQTSaeUuJJvQWAeo9TI/DtYzvp8AAAADAQABAAAAgBRXdq7kj/
+iR+WIEs7uifSMwuPGDjtxksg2Uj09kSGRLFmZdu4EWtvUh0uV0J37vbfBSkubU3fAvrP99
+bRxUHhD5Z444BIyht8jlBetfoJOBSE/TQJ/69xguSmHB8XH8/WUqEaNZ2F+q0AAkRt5CTs
+lkML/YJI1mPzy+0ny6tS8hAAAAQQD6N3CByknj5WrDIJQCce+zbhbftnN4RM6OBuaHv4mm
+y0qIAH7i6ZHFqHlr1OnCPzqtzIt4McoyIDEf+9eH3ktKAAAAQQD8uQ/jcs27tMPwb2GYyU
+vSNZ9g225seV+Y1dU+GSk8zuqmkN1Cbc0dmJW8hqncYrbDuFbRH3kPiLTLO3Aifn2xAAAA
+QQDsirzo7Ox+f2SC0TCu6+wbar6f617IhWRlUyPDNa18+ju+BHdw1890sRSpany5RbJekq
+KoumXchRmzl8vZPoVPAAAAHWluaWZjQFRoZS10b3hpYy1hdmVuZ2VyLmxvY2FsAQIDBAU=
+
+-----END OPENSSH PRIVATE KEY-----
diff --git a/cli/src/test/resources/hudson/cli/openssh-broken b/cli/src/test/resources/hudson/cli/openssh-broken
new file mode 100644
index 0000000000000000000000000000000000000000..f4ebcef2d10778153ea17af9d2f1390189f17dc4
--- /dev/null
+++ b/cli/src/test/resources/hudson/cli/openssh-broken
@@ -0,0 +1,16 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAIEA6YOQ5he3DIIgu+O9HJLvcGlLNbKe04rN7KftKhaALsqJAtS435bk
+xjv/ycn2uMnTapx2Q+Eu/wqATITB+SZEfAkgRMZmTa2Ze3zt/b6rieRhgRgovu3VyXsRnM
+fgCEuJqU5VRW5WlayYRUsJnQTSaeUuJJvQWAeo9TI/DtYzvp8AAAIYX2d44V9neOEAAAAH
+c3NoLXJzYQAAAIEA6YOQ5he3DIIgu+O9HJLvcGlLNbKe04rN7KftKhaALsqJAtS435bkxj
+v/ycn2uMnTapx2Q+Eu/wqATITB+SZEfAkgRMZmTa2Ze3zt/b6rieRhgRgovu3VyXsRnMfg
+CEuJqU5VRW5WlayYRUsJnQTSaeUuJJvQWAeo9TI/DtYzvp8AAAADAQABAAAAgBRXdq7kj/
+iR+WIEs7uifSMwuPGDjtxksg2Uj09kSGRLFmZdu4EWtvUh0uV0J37vbfBSkubU3fAvrP99
+bRxUHhD5Z444BIyht8jlBetfoJOBSE/TQJ/69xguSmHB8XH8/WUqEaNZ2F+q0AAkRt5CTs
+y0qIAH7i6ZHFqHlr1OnCPzqtzIt4McoyIDEf+9eH3ktKAAAAQQD8uQ/jcs27tMPwb2GYyU
+vSNZ9g225seV+Y1dU+GSk8zuqmkN1Cbc0dmJW8hqncYrbDuFbRH3kPiLTLO3Aifn2xAAAA
+QQDsirzo7Ox+f2SC0TCu6+wbar6f617IhWRlUyPDNa18+ju+BHdw1890sRSpany5RbJekq
+KoumXchRmzl8vZPoVPAAAAHWluaWZjQFRoZS10b3hpYy1hdmVuZ2VyLmxvY2FsAQIDBAU=
+
+-----END OPENSSH PRIVATE KEY-----
diff --git a/cli/src/test/resources/hudson/cli/openssh-unsuported b/cli/src/test/resources/hudson/cli/openssh-unsuported
new file mode 100644
index 0000000000000000000000000000000000000000..4ed6878f12c88cf120eb680b5cca53e904ff54cb
--- /dev/null
+++ b/cli/src/test/resources/hudson/cli/openssh-unsuported
@@ -0,0 +1,17 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDIzkiSdr
+mRRbRY1S2YtBacAAAAEAAAAAEAAACXAAAAB3NzaC1yc2EAAAADAQABAAAAgQCpXxUyU7MR
+b4GrSMnufZb6kaBb1lHBywI7+dBNnpHyq1rpZJRi36lpxxZoaiG+inOeSBh7QPnDVvm2aN
+DdT7V0PFzfZKWzxl2PRSd6EIXUsaaXuqaFcJM7rSqU1EB+9qM9JfGIqkdNKwzGdu3kdJJG
+3VQQUQVtYXAOLIrApslndQAAAiDAF+/lDARNPSMcOlH6uGqfSBszQBwOf22df5yGHLbnpi
+e5yslN+9Z3jFrl2XNDb8sD9e7gZtB/CbkJaDqFCqSOoW979xLLl89jJYle2L48SBbpV7i2
+0/51YYrxs4/75kJUd8uMaOBpNanyI8CBqA5IPmms1NLrSOpAbU20imQNoLUb1B8v2zNBxo
+R64UyU8AkKDMwPmAHwdrM73c7eirAsuVknhg0fFjy4iCxurqz5RO0Hpjf8GHfaCh37gim/
+8f/XqwS+MlAn9KaAGi4hZJFaSuyPPVLmRTS+gleoM0zSt5J+Fvdrs/JnL/XIpf64QZafKZ
+HyiZXnbJKxWhhhpslHz6QoiZ76LxwRS6bP//fsl7KWPY6IUGMD8h6JGi8o6xpsj16xIGUf
+7K+c9TNcjzaO8jtC8ggaixoKdC48LdenfdaagAtBgyWQqxfxTg9ZLiQ2lrZIaF4C0aZi3+
+byeT/mzPyh6aSAsBETjHCbdy/ixi0BNbDB10LlD44J8i9yBROk7WQtM3lJL78Va4KfxSCC
+dsJQPu1ABZEr1SGVqVprUaM43C/VKZk5PjXmkbAy5TCztztiWwa8HtKk4V7sH4G/ENjAtJ
+w2SEybRTx0WNhD9viTSu8z+BqH1mWP2ek/orE0OWU+H6r1zSoBo9scvpUrLC2NYvcUMw+I
+cDupua/5kKQgTzY+kEyRClwvziHEVe4TLZen3UMf2GM3fVC9AUC8
+-----END OPENSSH PRIVATE KEY-----
diff --git a/cli/src/test/resources/hudson/cli/rsa b/cli/src/test/resources/hudson/cli/rsa
new file mode 100644
index 0000000000000000000000000000000000000000..850ecb0e3d710f1477eb482677b041c8de4e5bd9
--- /dev/null
+++ b/cli/src/test/resources/hudson/cli/rsa
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQDcPcvFMiF7OsfaV9MuHQytEOffnyyFraVyYJrgRsdMsFMJcUVJ
+JY2oo2OdMF42CzstSe1fasAZnxalkGBvSdfH6PphgIhBcVj1eCgPR/76q2OyuRk7
+GE5R8GDlQSCsFsV658zyoBXELVFL9HvhXOw/0u7h60h/ertIoYawz44e3wIDAQAB
+AoGBAKTP7bQ07o88DqCbRmJkxL6iPxK+F+A1cPDl0CBzduMxtAIF7LZvTtHa60mP
+D4Fb6D3c67CSvwytW5IsN64wUTNaB6pOXmVnscgsXzNXz4UGsxsEOFUcYkEGJZ8M
+EMKDSqaCI43EMR7nJi/UNrJyj37Axn4cU3bgnDP5DAxO+AK5AkEA7kWA7zw86AoR
+vuSSWEINNSglzTRLZjciDX2b93kh97voRV0q/CHIoSvB3sLU0R/oLpOjaUhPFWrM
+JfI9GQemNQJBAOyg3MHBQhAd1rhXzNkW62903ICjlNGLOTemi+E11iIfGyA0bhLH
+DJQfR+Tx/VLQEVbAMU1pkUg33GRck4SPA0MCQHbmuDCqHrqsS6624VCppW2hWzvL
+nNSlLpkM1YfpKso1OvNiStEHCtdivpwrHYg+I98aTbF8I/rMEJPfDh4vcwECQQCv
+LBbAyNSjIbPHHBhlzXXVOOnTwUV2Kl7dN8ntmvE+qVBncujZtck2DkIm1o32NFnh
+or3c1P3cPJ5HHdGHHGgJAkBdbOMa8VIFBliHr22akFD43gakZ/7ymRNTXA1uj6xt
+NO/1mklbaj4u41UQZU9JjsqebOMGnIc88KMFh6wdfaed
+-----END RSA PRIVATE KEY-----
diff --git a/cli/src/test/resources/hudson/cli/rsa-password b/cli/src/test/resources/hudson/cli/rsa-password
new file mode 100644
index 0000000000000000000000000000000000000000..55c3f7568176f238069be9abc1d01ef4eb9455d5
--- /dev/null
+++ b/cli/src/test/resources/hudson/cli/rsa-password
@@ -0,0 +1,18 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,B937F90AFDFDE86CD9023373F5079F8F
+
+N6ohIfZ+nmqodFzFlFCG6gBXEGIBOl/ojaV+GSHpCJRKJs6lgGSwVmSHESqiKpAI
+ZNQpWEURIVXRpScAuqB+BNQyNjnlFwoo47aBkfDFR2DTY6y+21Z1Fll9ZExABgRJ
+IsMoRVjVbd4eJ1njln73b290EUBNu4ej14pPFsHqSdHQ4atPXw3Alph0NuFYz1CI
+Jg/0vUFFX1u2UjLYAHRU7EKJUk027H1GjZdlV7kcogMRblINokGK0rpmuize6fE9
+WHzEkeGb7qwU4fJ1Sa396TwA5sU6K7xPqV+sArWevavVC4xi0yKoSMmzR2ps5rg+
+XDNBnscNo0dTxnR/Dku2fsqiTsvXZ/LQDWTyTkC0Sx/gYKmfmYfmOpFh6pW0PRUF
+h8Cxm/XkAZV90pK+SrwKA/Pj2vi5nJvAsQlHD+5f7GYLMiB2pWJdbTuXjg0MJjvP
+3TMZm7SqOMe7G4iyXKmlg7Hri2jMPkzvmoGMQZk8bFoX87579w4s0bCdP23lcI2H
+6F7OHTHnPbPc302vN2NWARuqP6XJGRlwJJaNkL8/cvMh42UeVZEZ5WwacUW5g0IN
+IE+K/L5EemBVI2whRsgBBxbyKymBMTolKDhLbGicxAb7E1jr3UXsRv17LaoaPMDN
+TbCjhRkXiYAWXzR/BVwG2vhCI9geCXbLXAVhp2U/z6+KDAVXUIf2Hwz1v1g+fabV
+DGQBzCobfJv9ML9/oO6+KP/dgAA/kBMsLNmQTAR9NslDqdbVujNJ0fSvqaEG1lbq
+7oc36BIyjJgPEehE0kCfzAeVRHq73N8lfbZaz5cnvdMjCXwMubcIL1kBfUpv5eSg
+-----END RSA PRIVATE KEY-----
diff --git a/core/move-l10n.groovy b/core/move-l10n.groovy
index d774ec50d4817573f8d4ba37a33632745a1f63b9..c52e66ae53aecf6318efc70b079e6777df580bd6 100644
--- a/core/move-l10n.groovy
+++ b/core/move-l10n.groovy
@@ -13,6 +13,7 @@ for (p in new File(resDir, oldview).parentFile.listFiles()) {
def n = p.name;
if (n == "${basename}.properties" || n.startsWith("${basename}_") && n.endsWith(".properties")) {
def lines = p.readLines('ISO-8859-1');
+ // TODO does not handle multiline values correctly
def matches = lines.findAll({it.startsWith("${key}=")});
if (!matches.isEmpty()) {
lines.removeAll(matches);
@@ -24,6 +25,7 @@ for (p in new File(resDir, oldview).parentFile.listFiles()) {
} else {
def nue = new File(resDir, newview + n.substring(basename.length()));
println("moving ${matches.size()} matches from ${n} to ${nue.name}");
+ // TODO if the original lacked a trailing newline, this will corrupt previously final key
nue.withWriterAppend('ISO-8859-1') {out ->
matches.each {line -> out.writeLine(line)}
}
diff --git a/core/pom.xml b/core/pom.xml
index f8d7e4eb465cde17dd55fe9283a8fd8a413dc77b..388f0a7930733d97e6c564d601511bcc3e01f9ca 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -28,8 +28,8 @@ THE SOFTWARE.
org.jenkins-ci.main
- pom
- 2.54-SNAPSHOT
+ jenkins-parent
+ ${revision}${changelist}jenkins-core
@@ -39,11 +39,9 @@ THE SOFTWARE.
true
- 1.250
+ 1.2562.5.6.SEC03
- 2.4.8
-
- true
+ 2.4.12
@@ -65,7 +63,7 @@ THE SOFTWARE.
org.jenkins-civersion-number
- 1.3
+ 1.6org.jenkins-ci
@@ -95,6 +93,12 @@ THE SOFTWARE.
com.google.injectguice
+
+
+ com.google.guava
+ guava
+
+
@@ -105,7 +109,7 @@ THE SOFTWARE.
com.github.jnrjnr-posix
- 3.0.1
+ 3.0.45org.kohsuke
@@ -115,7 +119,7 @@ THE SOFTWARE.
org.jenkins-citrilead-ssh2
- build217-jenkins-8
+ build-217-jenkins-14org.kohsuke.stapler
@@ -173,6 +177,17 @@ THE SOFTWARE.
teststest
+
+ io.jenkins.stapler
+ jenkins-stapler-support
+ 1.0
+
+
+ org.hamcrest
+ hamcrest-library
+ 1.3
+ test
+ com.infradna.tool
@@ -197,22 +212,21 @@ THE SOFTWARE.
org.jenkins-ciannotation-indexer
- 1.11org.jenkins-cibytecode-compatibility-transformer
- 1.8
+ 2.0-beta-2org.jenkins-citask-reactor
- 1.4
+ 1.5org.jvnet.localizerlocalizer
- 1.24
+ 1.26antlr
@@ -234,6 +248,20 @@ THE SOFTWARE.
+
+
+ xpp3
+ xpp3
+ 1.1.4c
+
+
+ net.sf.kxml
+ kxml2
+ 2.3.0
+ jfreejfreechart
@@ -256,7 +284,6 @@ THE SOFTWARE.
commons-langcommons-lang
- 2.6commons-digester
@@ -440,11 +467,6 @@ THE SOFTWARE.
spring-aop${spring.version}
-
- xpp3
- xpp3
- 1.1.4c
- junitjunit
@@ -462,13 +484,13 @@ THE SOFTWARE.
org.powermock
- powermock-api-mockito
+ powermock-api-mockito2test
- javax.servlet
- jstl
- 1.1.0
+ javax.servlet.jsp.jstl
+ javax.servlet.jsp.jstl-api
+ 1.2.1org.slf4j
@@ -496,7 +518,7 @@ THE SOFTWARE.
org.jvnet.winpwinp
- 1.24
+ 1.27org.jenkins-ci
@@ -516,7 +538,7 @@ THE SOFTWARE.
net.java.dev.jnajna
- 4.2.1
+ 4.5.2org.kohsuke
@@ -526,12 +548,12 @@ THE SOFTWARE.
org.kohsukelibpam4j
- 1.8
+ 1.11
- org.jvnet.libzfs
+ org.kohsukelibzfs
- 0.5
+ 0.8com.sun.solaris
@@ -541,7 +563,7 @@ THE SOFTWARE.
net.java.sezpozsezpoz
- 1.12
+ 1.13org.kohsuke.jinterop
@@ -551,8 +573,7 @@ THE SOFTWARE.
org.kohsuke.metainf-servicesmetainf-services
- 1.4
- provided
+ 1.8true
@@ -566,29 +587,26 @@ THE SOFTWARE.
1.1
-
+ commons-codeccommons-codec
- 1.8org.kohsukeaccess-modifier-annotation
- 1.4com.google.code.findbugsannotations
- 3.0.0providedcommons-fileuploadcommons-fileupload
- 1.3.1-jenkins-1
+ 1.3.1-jenkins-2
@@ -603,6 +621,12 @@ THE SOFTWARE.
com.google.guavaguava
+
+
+ com.google.code.findbugs
+ jsr305
+
+ com.google.guava
@@ -704,7 +728,6 @@ THE SOFTWARE.
org.kohsukeaccess-modifier-checker
-
@@ -755,7 +778,7 @@ THE SOFTWARE.
com.sun.winswwinsw
- 2.0.3
+ 2.2.0binexe${project.build.outputDirectory}/windows-service
@@ -772,7 +795,6 @@ THE SOFTWARE.
0.5Ctrue-noverify
- false
@@ -788,25 +810,6 @@ THE SOFTWARE.
-
- org.codehaus.gmaven
- gmaven-plugin
-
-
-
-
- testCompile
-
-
-
-
-
- org.codehaus.groovy
- groovy-all
- ${groovy.version}
-
-
- org.codehaus.mojofindbugs-maven-plugin
@@ -881,41 +884,5 @@ THE SOFTWARE.
true
-
-
- cobertura
-
-
-
- org.codehaus.gmaven
- gmaven-plugin
-
-
-
-
- test
-
- execute
-
-
-
- ${project.basedir}/src/build-script
-
-
-
-
-
-
-
-
- maven-surefire-plugin
-
-
- true
-
-
-
-
-
diff --git a/core/src/build-script/Cobertura.groovy b/core/src/build-script/Cobertura.groovy
deleted file mode 100644
index 49b773b49ba17e79fa74975cf8a0b20d42153143..0000000000000000000000000000000000000000
--- a/core/src/build-script/Cobertura.groovy
+++ /dev/null
@@ -1,82 +0,0 @@
-import org.apache.maven.project.MavenProject;
-
-/**
- * Cobertura invoker.
- */
-public class Cobertura {
- private final MavenProject project;
- // maven helper
- private def maven;
- // ant builder
- private def ant;
- /**
- * Cobertura data file.
- */
- private final File ser;
-
- def Cobertura(project, maven, ant, ser) {
- this.project = maven.project;
- this.maven = maven;
- this.ant = ant;
- this.ser =ser;
-
- // define cobertura tasks
- ant.taskdef(resource:"tasks.properties")
- }
-
- // function that ensures that the given directory exists
- private String dir(String dir) {
- new File(project.basedir,dir).mkdirs();
- return dir;
- }
-
- /**
- * Instruments the given class dirs/jars by cobertura
- *
- * @param files
- * List of jar files and class dirs to instrument.
- */
- def instrument(files) {
- ant."cobertura-instrument"(todir:dir("target/cobertura-classes"),datafile:ser) {
- fileset(dir:"target/classes");
- files.each{ fileset(file:it) }
- }
- }
-
- def runTests() {
- ant.junit(fork:true, forkMode:"once", failureproperty:"failed", printsummary:true) {
- classpath {
- junitClasspath()
- }
- batchtest(todir:dir("target/surefire-reports")) {
- fileset(dir:"target/test-classes") {
- include(name:"**/*Test.class")
- }
- formatter(type:"xml")
- }
- sysproperty(key:"net.sourceforge.cobertura.datafile",value:ser)
- sysproperty(key:"hudson.ClassicPluginStrategy.useAntClassLoader",value:"true")
- }
- }
-
- def junitClasspath() {
- ant.pathelement(path: "target/cobertura-classes") // put the instrumented classes first
- ant.fileset(dir:"target/cobertura-classes",includes:"*.jar") // instrumented jar files
- ant.pathelement(path: maven.resolveArtifact("net.sourceforge.cobertura:cobertura:1.9")) // cobertura runtime
- project.getTestClasspathElements().each { ant.pathelement(path: it) } // the rest of the dependencies
- }
-
- def report(dirs) {
- maven.attachArtifact(ser,"ser","cobertura")
- ["html","xml"].each { format ->
- ant."cobertura-report"(format:format,datafile:ser,destdir:dir("target/cobertura-reports"),srcdir:"src/main/java") {
- dirs.each{ fileset(dir:it) }
- }
- }
- }
-
- def makeBuildFailIfTestFail() {
- if(ant.project.getProperty("failed")!=null && !Boolean.getBoolean("testFailureIgnore"))
- throw new Exception("Some unit tests failed");
- }
-}
diff --git a/core/src/build-script/readme.txt b/core/src/build-script/readme.txt
deleted file mode 100644
index 6c53a01026b7921e493e1c42f7463c0857c17c69..0000000000000000000000000000000000000000
--- a/core/src/build-script/readme.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-Parts of the build scripts that are written in Groovy.
-
-IntelliJ needs Groovy files to be in its source directory to provide completion and such,
-so we need this to be in its own directory.
\ No newline at end of file
diff --git a/core/src/build-script/unitTest.groovy b/core/src/build-script/unitTest.groovy
deleted file mode 100644
index 653391aad7f15bbc6cdea2366f5f143bd4a0ae2b..0000000000000000000000000000000000000000
--- a/core/src/build-script/unitTest.groovy
+++ /dev/null
@@ -1,11 +0,0 @@
-// run unit tests
-
-ant.project.setBaseDir(project.basedir)
-ser=new File(project.basedir,"target/cobertura.ser"); // store cobertura data file in a module-specific location
-
-cob = new Cobertura(project,maven,ant,ser);
-
-cob.instrument([])
-cob.runTests()
-cob.report([])
-cob.makeBuildFailIfTestFail();
diff --git a/core/src/filter/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_it.properties b/core/src/filter/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_it.properties
index 7378e405c76f9e21a930a2e698b8baf8f024a484..510a7f5f861f964e339da3ac0755dab0a1779d10 100644
--- a/core/src/filter/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_it.properties
+++ b/core/src/filter/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_it.properties
@@ -1,27 +1,6 @@
-# The MIT License
-#
-# Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Giulio D'Ambrosi
-#
-# 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.
-
-NewVersionAvailable=C''\u00E8 una nuova versione di Jenkins ({0}) disponibile per il download (changelog).
-Or\ Upgrade\ Automatically=Oppure aggiorna automaticamente
-UpgradeComplete=Aggiornamento a Jenkins {0} completato, in attesa di riavvio.
-UpgradeCompleteRestartNotSupported=Aggiornamento a Jenkins {0} completato, in attesa di riavvio.
-UpgradeProgress=Aggiornamento a Jenkins {0} in corso oppure fallito.
+NewVersionAvailable=Una nuova versione di Jenkins ({0}) disponibile per il download \
+ (log delle modifiche).
+UpgradeComplete=Aggiornamento a Jenkins {0} completato, in attesa del riavvio.
+UpgradeCompleteRestartNotSupported=Aggiornamento a Jenkins {0} completato, in attesa del riavvio.
+UpgradeProgress=Aggiornamento a Jenkins {0} in corso.
+UpgradeFailed=Aggiornamento a Jenkins {0} non riuscito: {1}.
diff --git a/core/src/filter/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_zh_CN.properties b/core/src/filter/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_zh_CN.properties
deleted file mode 100644
index 0ef79b530b974daaaed8ae9bc346ed86fb71a2bc..0000000000000000000000000000000000000000
--- a/core/src/filter/resources/hudson/model/UpdateCenter/CoreUpdateMonitor/message_zh_CN.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-# This file is under the MIT License by authors
-
-NewVersionAvailable=Jenkins\u65B0\u7248\u672C ({0})\u53EF\u70B9\u51FB download (\u53D8\u66F4\u8BF4\u660E)\u4E0B\u8F7D\u3002
-Or\ Upgrade\ Automatically=\u6216 \u81EA\u52A8\u5347\u7EA7\u7248\u672C
-UpgradeComplete=Jenkins {0} \u7248\u672C\u5347\u7EA7\u5DF2\u5B8C\u6210,\u7B49\u5F85\u91CD\u542F\u4E2D.
-UpgradeCompleteRestartNotSupported=Jenkins {0} \u7248\u672C\u5347\u7EA7\u5DF2\u5B8C\u6210,\u7B49\u5F85\u91CD\u542F\u4E2D.
diff --git a/core/src/filter/resources/hudson/model/hudson-version.properties b/core/src/filter/resources/hudson/model/hudson-version.properties
index 9b9343d10d659357b7acccbcf34bc2aece573625..defbd48204e4784090fe0e17e28fe5bc395abf47 100644
--- a/core/src/filter/resources/hudson/model/hudson-version.properties
+++ b/core/src/filter/resources/hudson/model/hudson-version.properties
@@ -1 +1 @@
-version=${build.version}
\ No newline at end of file
+version=${project.version}
diff --git a/core/src/filter/resources/jenkins/model/jenkins-version.properties b/core/src/filter/resources/jenkins/model/jenkins-version.properties
index 9b9343d10d659357b7acccbcf34bc2aece573625..defbd48204e4784090fe0e17e28fe5bc395abf47 100644
--- a/core/src/filter/resources/jenkins/model/jenkins-version.properties
+++ b/core/src/filter/resources/jenkins/model/jenkins-version.properties
@@ -1 +1 @@
-version=${build.version}
\ No newline at end of file
+version=${project.version}
diff --git a/core/src/filter/resources/jenkins/slaves/remoting-info.properties b/core/src/filter/resources/jenkins/slaves/remoting-info.properties
new file mode 100644
index 0000000000000000000000000000000000000000..6ac78f4ad43e96e9f1277206b67049f098979a21
--- /dev/null
+++ b/core/src/filter/resources/jenkins/slaves/remoting-info.properties
@@ -0,0 +1,6 @@
+# Remoting version, which is embedded into the core
+# This version MAY differ from what is really classloaded (see the "Pluggable Core Components", JENKINS-41196)
+remoting.embedded.version=${remoting.version}
+
+# Minimum Remoting version on external agents which is supported by the core
+remoting.minimum.supported.version=${remoting.minimum.supported.version}
diff --git a/core/src/main/groovy/hudson/util/LoadMonitor.groovy b/core/src/main/groovy/hudson/util/LoadMonitor.groovy
deleted file mode 100644
index 97d701816199c011e67c6fcc39e539563442f225..0000000000000000000000000000000000000000
--- a/core/src/main/groovy/hudson/util/LoadMonitor.groovy
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * The MIT License
- *
- * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
- *
- * 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 hudson.util
-
-import hudson.model.Computer
-import jenkins.util.Timer
-import jenkins.model.Jenkins
-import hudson.model.Label
-import hudson.model.Queue.BlockedItem
-import hudson.model.Queue.BuildableItem
-import hudson.model.Queue.WaitingItem
-import hudson.triggers.SafeTimerTask
-import java.text.DateFormat
-import java.util.concurrent.TimeUnit
-
-/**
- * Spits out the load information.
- *
- *
- * I'm using this code to design the auto scaling feature.
- * In future this might be useful data to expose to the UI.
- *
- * @author Kohsuke Kawaguchi
- */
-public class LoadMonitorImpl extends SafeTimerTask {
-
- private final File dataFile;
- private List labels;
-
- public LoadMonitorImpl(File dataFile) {
- this.dataFile = dataFile;
- labels = Jenkins.getInstance().labels*.name;
- printHeaders();
- Timer.get().scheduleAtFixedRate(this,0,10*1000, TimeUnit.MILLISECONDS);
- }
-
- private String quote(Object s) { "\"${s}\""; }
-
- protected void printHeaders() {
- def headers = ["# of executors","# of busy executors","BuildableItems in Q","BuildableItem avg wait time"];
- def data = ["timestamp"];
- data += headers;
- data += ["WaitingItems in Q","BlockedItems in Q"];
-
- for( String label : labels)
- data += headers.collect { "${it} (${label}}" }
-
- dataFile.append(data.collect({ quote(it) }).join(",")+"\n");
- }
-
- @Override
- protected void doRun() {
- def now = new Date();
- def data = [];
- data.add(quote(FORMATTER.format(now)));
-
- def h = Jenkins.getInstance();
-
- def items = h.queue.items;
- def filterByType = {Class type -> items.findAll { type.isInstance(it) } }
-
- def builder = {List cs, Closure itemFilter ->
- // number of total executor, number of busy executor
- data.add(cs.sum { it.isOffline() ? 0 : it.numExecutors });
- data.add(cs.sum {Computer c ->
- c.executors.findAll { !it.isIdle() }.size()
- });
-
- // queue statistics
- def is = filterByType(BuildableItem).findAll(itemFilter);
- data.add(is.size());
- data.add(is.sum {BuildableItem bi -> now.time - bi.buildableStartMilliseconds }?:0 / Math.max(1,is.size()) );
- };
-
-
- // for the whole thing
- builder(Arrays.asList(h.computers),{ it->true });
-
- data.add(filterByType(WaitingItem).size());
- data.add(filterByType(BlockedItem).size());
-
- // per label stats
- for (String label : labels) {
- Label l = h.getLabel(label)
- builder(l.nodes.collect { it.toComputer() }) { BuildableItem bi -> bi.task.assignedLabel==l };
- }
-
- dataFile.append(data.join(",")+"\n");
- }
-
- private static final DateFormat FORMATTER = DateFormat.getDateTimeInstance();
-}
-
-new LoadMonitorImpl(new File("/files/hudson/load.txt"));
-
diff --git a/core/src/main/groovy/jenkins/util/groovy/AbstractGroovyViewModule.groovy b/core/src/main/groovy/jenkins/util/groovy/AbstractGroovyViewModule.groovy
deleted file mode 100644
index 08cb1c23a3da552cc531cf3b655b0a915b43aee6..0000000000000000000000000000000000000000
--- a/core/src/main/groovy/jenkins/util/groovy/AbstractGroovyViewModule.groovy
+++ /dev/null
@@ -1,47 +0,0 @@
-package jenkins.util.groovy
-
-import lib.FormTagLib
-import lib.LayoutTagLib
-import org.kohsuke.stapler.jelly.groovy.JellyBuilder
-import org.kohsuke.stapler.jelly.groovy.Namespace
-import lib.JenkinsTagLib
-
-/**
- * Base class for utility classes for Groovy view scripts
- *
- * Usage from script of a subclass, say ViewHelper:
- *
- * new ViewHelper(delegate).method();
- *
- * see ModularizeViewScript in ui-samples for an example how to use this class.
- *
- * @author Stefan Wolf (wolfs)
- */
-abstract class AbstractGroovyViewModule {
- JellyBuilder builder
- FormTagLib f
- LayoutTagLib l
- JenkinsTagLib t
- Namespace st
-
- public AbstractGroovyViewModule(JellyBuilder b) {
- builder = b
- f= builder.namespace(FormTagLib)
- l=builder.namespace(LayoutTagLib)
- t=builder.namespace(JenkinsTagLib)
- st=builder.namespace("jelly:stapler")
-
- }
-
- def methodMissing(String name, args) {
- builder.invokeMethod(name,args)
- }
-
- def propertyMissing(String name) {
- builder.getProperty(name)
- }
-
- def propertyMissing(String name, value) {
- builder.setProperty(name, value)
- }
-}
diff --git a/core/src/main/java/hudson/AbstractMarkupText.java b/core/src/main/java/hudson/AbstractMarkupText.java
index ed7e319711040b7bbb8ba657a7e1227dca85d626..2398f6f7446972a1f724a0ee85fc58d251f1b22e 100644
--- a/core/src/main/java/hudson/AbstractMarkupText.java
+++ b/core/src/main/java/hudson/AbstractMarkupText.java
@@ -72,8 +72,8 @@ public abstract class AbstractMarkupText {
* Adds a start tag and end tag at the specified position.
*
*
- * For example, if the text was "abc", then addMarkup(1,2,"<b>","</b>")
- * would generate "a<b>b</b>c"
+ * For example, if the text was "abc", then {@code addMarkup(1,2,"","")}
+ * would generate {@code"abc"}
*/
public abstract void addMarkup( int startPos, int endPos, String startTag, String endTag );
@@ -141,7 +141,7 @@ public abstract class AbstractMarkupText {
public List findTokens(Pattern pattern) {
String text = getText();
Matcher m = pattern.matcher(text);
- List r = new ArrayList();
+ List r = new ArrayList<>();
while(m.find()) {
int idx = m.start();
diff --git a/core/src/main/java/hudson/BulkChange.java b/core/src/main/java/hudson/BulkChange.java
index 28b9fde4261074ca2e4828534139d16e18c8f464..e65fa4d3acb6d96705125c78274e129d53352235 100644
--- a/core/src/main/java/hudson/BulkChange.java
+++ b/core/src/main/java/hudson/BulkChange.java
@@ -132,7 +132,7 @@ public class BulkChange implements Closeable {
/**
* {@link BulkChange}s that are effective currently.
*/
- private static final ThreadLocal INSCOPE = new ThreadLocal();
+ private static final ThreadLocal INSCOPE = new ThreadLocal<>();
/**
* Gets the {@link BulkChange} instance currently in scope for the current thread.
diff --git a/core/src/main/java/hudson/ClassicPluginStrategy.java b/core/src/main/java/hudson/ClassicPluginStrategy.java
index 17722cef44742cb0561288fad27f5be94cdbc0d5..1d93d3fb249cce72a88725bab80d4b9ef523d768 100644
--- a/core/src/main/java/hudson/ClassicPluginStrategy.java
+++ b/core/src/main/java/hudson/ClassicPluginStrategy.java
@@ -23,21 +23,21 @@
*/
package hudson;
-import java.io.InputStream;
-import java.nio.file.Files;
-import jenkins.util.SystemProperties;
import com.google.common.collect.Lists;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Plugin.DummyImpl;
import hudson.PluginWrapper.Dependency;
import hudson.model.Hudson;
-import jenkins.util.AntClassLoader;
import hudson.util.CyclicGraphDetector;
import hudson.util.CyclicGraphDetector.CycleDetectedException;
import hudson.util.IOUtils;
import hudson.util.MaskingClassLoader;
-import hudson.util.VersionNumber;
import jenkins.ClassLoaderReflectionToolkit;
import jenkins.ExtensionFilter;
+import jenkins.plugins.DetachedPluginsUtil;
+import jenkins.util.AntClassLoader;
+import jenkins.util.AntWithFindResourceClassLoader;
+import jenkins.util.SystemProperties;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
@@ -52,38 +52,36 @@ import org.apache.tools.ant.util.GlobPatternMapper;
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipExtraField;
import org.apache.tools.zip.ZipOutputStream;
+import org.jenkinsci.bytecode.Transformer;
+import javax.annotation.Nonnull;
import java.io.Closeable;
import java.io.File;
-import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
-import java.lang.reflect.Field;
+import java.io.InputStream;
import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
-import java.util.Set;
-import java.util.Vector;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
-import org.jenkinsci.bytecode.Transformer;
-import org.kohsuke.accmod.Restricted;
-import org.kohsuke.accmod.restrictions.NoExternalUse;
-
-import javax.annotation.Nonnull;
import static org.apache.commons.io.FilenameUtils.getBaseName;
public class ClassicPluginStrategy implements PluginStrategy {
+ private static final Logger LOGGER = Logger.getLogger(ClassicPluginStrategy.class.getName());
+
/**
* Filter for jar files.
*/
@@ -106,11 +104,20 @@ public class ClassicPluginStrategy implements PluginStrategy {
@Override public String getShortName(File archive) throws IOException {
Manifest manifest;
+ if (!archive.exists()) {
+ throw new FileNotFoundException("Failed to load " + archive + ". The file does not exist");
+ } else if (!archive.isFile()) {
+ throw new FileNotFoundException("Failed to load " + archive + ". It is not a file");
+ }
+
if (isLinked(archive)) {
manifest = loadLinkedManifest(archive);
} else {
try (JarFile jf = new JarFile(archive, false)) {
manifest = jf.getManifest();
+ } catch (IOException ex) {
+ // Mention file name in the exception
+ throw new IOException("Failed to load " + archive, ex);
}
}
return PluginWrapper.computeShortName(manifest, archive.getName());
@@ -127,6 +134,8 @@ public class ClassicPluginStrategy implements PluginStrategy {
String firstLine;
try (InputStream manifestHeaderInput = Files.newInputStream(archive.toPath())) {
firstLine = IOUtils.readFirstLine(manifestHeaderInput, "UTF-8");
+ } catch (InvalidPathException e) {
+ throw new IOException(e);
}
if (firstLine.startsWith("Manifest-Version:")) {
// this is the manifest already
@@ -138,6 +147,8 @@ public class ClassicPluginStrategy implements PluginStrategy {
// Read the manifest
try (InputStream manifestInput = Files.newInputStream(archive.toPath())) {
return new Manifest(manifestInput);
+ } catch (InvalidPathException e) {
+ throw new IOException(e);
}
} catch (IOException e) {
throw new IOException("Failed to load " + archive, e);
@@ -171,6 +182,8 @@ public class ClassicPluginStrategy implements PluginStrategy {
}
try (InputStream fin = Files.newInputStream(manifestFile.toPath())) {
manifest = new Manifest(fin);
+ } catch (InvalidPathException e) {
+ throw new IOException(e);
}
}
@@ -179,7 +192,7 @@ public class ClassicPluginStrategy implements PluginStrategy {
// TODO: define a mechanism to hide classes
// String export = manifest.getMainAttributes().getValue("Export");
- List paths = new ArrayList();
+ List paths = new ArrayList<>();
if (isLinked) {
parseClassPath(manifest, archive, paths, "Libraries", ",");
parseClassPath(manifest, archive, paths, "Class-Path", " +"); // backward compatibility
@@ -187,8 +200,10 @@ public class ClassicPluginStrategy implements PluginStrategy {
baseResourceURL = resolve(archive,atts.getValue("Resource-Path")).toURI().toURL();
} else {
File classes = new File(expandDir, "WEB-INF/classes");
- if (classes.exists())
+ if (classes.exists()) { // should not normally happen, due to createClassJarFromWebInfClasses
+ LOGGER.log(Level.WARNING, "Deprecated unpacked classes directory found in {0}", classes);
paths.add(classes);
+ }
File lib = new File(expandDir, "WEB-INF/lib");
File[] libs = lib.listFiles(JAR_FILTER);
if (libs != null)
@@ -203,8 +218,8 @@ public class ClassicPluginStrategy implements PluginStrategy {
}
// compute dependencies
- List dependencies = new ArrayList();
- List optionalDependencies = new ArrayList();
+ List dependencies = new ArrayList<>();
+ List optionalDependencies = new ArrayList<>();
String v = atts.getValue("Plugin-Dependencies");
if (v != null) {
for (String s : v.split(",")) {
@@ -242,32 +257,16 @@ public class ClassicPluginStrategy implements PluginStrategy {
if (jenkinsVersion==null)
jenkinsVersion = atts.getValue("Hudson-Version");
- optionalDependencies.addAll(getImpliedDependencies(pluginName, jenkinsVersion));
+ optionalDependencies.addAll(DetachedPluginsUtil.getImpliedDependencies(pluginName, jenkinsVersion));
}
-
+
/**
- * Returns all the plugin dependencies that are implicit based on a particular Jenkins version
- * @since 2.0
+ * @see DetachedPluginsUtil#getImpliedDependencies(String, String)
*/
+ @Deprecated // since TODO
@Nonnull
public static List getImpliedDependencies(String pluginName, String jenkinsVersion) {
- List out = new ArrayList<>();
- for (DetachedPlugin detached : DETACHED_LIST) {
- // don't fix the dependency for itself, or else we'll have a cycle
- if (detached.shortName.equals(pluginName)) {
- continue;
- }
- if (BREAK_CYCLES.contains(pluginName + '/' + detached.shortName)) {
- LOGGER.log(Level.FINE, "skipping implicit dependency {0} → {1}", new Object[] {pluginName, detached.shortName});
- continue;
- }
- // some earlier versions of maven-hpi-plugin apparently puts "null" as a literal in Hudson-Version. watch out for them.
- if (jenkinsVersion == null || jenkinsVersion.equals("null") || new VersionNumber(jenkinsVersion).compareTo(detached.splitWhen) <= 0) {
- out.add(new PluginWrapper.Dependency(detached.shortName + ':' + detached.requiredVersion));
- LOGGER.log(Level.FINE, "adding implicit dependency {0} → {1} because of {2}", new Object[] {pluginName, detached.shortName, jenkinsVersion});
- }
- }
- return out;
+ return DetachedPluginsUtil.getImpliedDependencies(pluginName, jenkinsVersion);
}
@Deprecated
@@ -295,138 +294,6 @@ public class ClassicPluginStrategy implements PluginStrategy {
return classLoader;
}
- /**
- * Get the list of all plugins that have ever been {@link DetachedPlugin detached} from Jenkins core.
- * @return A {@link List} of {@link DetachedPlugin}s.
- */
- @Restricted(NoExternalUse.class)
- public static @Nonnull List getDetachedPlugins() {
- return DETACHED_LIST;
- }
-
- /**
- * Get the list of plugins that have been detached since a specific Jenkins release version.
- * @param since The Jenkins version.
- * @return A {@link List} of {@link DetachedPlugin}s.
- */
- @Restricted(NoExternalUse.class)
- public static @Nonnull List getDetachedPlugins(@Nonnull VersionNumber since) {
- List detachedPlugins = new ArrayList<>();
-
- for (DetachedPlugin detachedPlugin : DETACHED_LIST) {
- if (!detachedPlugin.getSplitWhen().isOlderThan(since)) {
- detachedPlugins.add(detachedPlugin);
- }
- }
-
- return detachedPlugins;
- }
-
- /**
- * Is the named plugin a plugin that was detached from Jenkins at some point in the past.
- * @param pluginId The plugin ID.
- * @return {@code true} if the plugin is a plugin that was detached from Jenkins at some
- * point in the past, otherwise {@code false}.
- */
- @Restricted(NoExternalUse.class)
- public static boolean isDetachedPlugin(@Nonnull String pluginId) {
- for (DetachedPlugin detachedPlugin : DETACHED_LIST) {
- if (detachedPlugin.getShortName().equals(pluginId)) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Information about plugins that were originally in the core.
- *
- * A detached plugin is one that has any of the following characteristics:
- *
- *
- * Was an existing plugin that at some time previously bundled with the Jenkins war file.
- *
- *
- * Was previous code in jenkins core that was split to a separate-plugin (but may not have
- * ever been bundled in a jenkins war file - i.e. it gets split after this 2.0 update).
- *
- *
- */
- @Restricted(NoExternalUse.class)
- public static final class DetachedPlugin {
- private final String shortName;
- /**
- * Plugins built for this Jenkins version (and earlier) will automatically be assumed to have
- * this plugin in its dependency.
- *
- * When core/pom.xml version is 1.123-SNAPSHOT when the code is removed, then this value should
- * be "1.123.*" (because 1.124 will be the first version that doesn't include the removed code.)
- */
- private final VersionNumber splitWhen;
- private final String requiredVersion;
-
- private DetachedPlugin(String shortName, String splitWhen, String requiredVersion) {
- this.shortName = shortName;
- this.splitWhen = new VersionNumber(splitWhen);
- this.requiredVersion = requiredVersion;
- }
-
- /**
- * Get the short name of the plugin.
- * @return The short name of the plugin.
- */
- public String getShortName() {
- return shortName;
- }
-
- /**
- * Get the Jenkins version from which the plugin was detached.
- * @return The Jenkins version from which the plugin was detached.
- */
- public VersionNumber getSplitWhen() {
- return splitWhen;
- }
-
- /**
- * Gets the minimum required version for the current version of Jenkins.
- *
- * @return the minimum required version for the current version of Jenkins.
- * @sice 2.16
- */
- public VersionNumber getRequiredVersion() {
- return new VersionNumber(requiredVersion);
- }
- }
-
- private static final List DETACHED_LIST = Collections.unmodifiableList(Arrays.asList(
- new DetachedPlugin("maven-plugin", "1.296", "1.296"),
- new DetachedPlugin("subversion", "1.310", "1.0"),
- new DetachedPlugin("cvs", "1.340", "0.1"),
- new DetachedPlugin("ant", "1.430.*", "1.0"),
- new DetachedPlugin("javadoc", "1.430.*", "1.0"),
- new DetachedPlugin("external-monitor-job", "1.467.*", "1.0"),
- new DetachedPlugin("ldap", "1.467.*", "1.0"),
- new DetachedPlugin("pam-auth", "1.467.*", "1.0"),
- new DetachedPlugin("mailer", "1.493.*", "1.2"),
- new DetachedPlugin("matrix-auth", "1.535.*", "1.0.2"),
- new DetachedPlugin("windows-slaves", "1.547.*", "1.0"),
- new DetachedPlugin("antisamy-markup-formatter", "1.553.*", "1.0"),
- new DetachedPlugin("matrix-project", "1.561.*", "1.0"),
- new DetachedPlugin("junit", "1.577.*", "1.0"),
- new DetachedPlugin("bouncycastle-api", "2.16.*", "2.16.0")
- ));
-
- /** Implicit dependencies that are known to be unnecessary and which must be cut out to prevent a dependency cycle among bundled plugins. */
- private static final Set BREAK_CYCLES = new HashSet(Arrays.asList(
- "script-security/matrix-auth",
- "script-security/windows-slaves",
- "script-security/antisamy-markup-formatter",
- "script-security/matrix-project",
- "credentials/matrix-auth",
- "credentials/windows-slaves"
- ));
-
/**
* Computes the classloader that takes the class masking into account.
*
@@ -457,7 +324,7 @@ public class ClassicPluginStrategy implements PluginStrategy {
* See {@link ExtensionFinder#scout(Class, Hudson)} for the dead lock issue and what this does.
*/
if (LOGGER.isLoggable(Level.FINER))
- LOGGER.log(Level.FINER,"Scout-loading ExtensionList: "+type, new Throwable());
+ LOGGER.log(Level.FINER, "Scout-loading ExtensionList: "+type, new Throwable());
for (ExtensionFinder finder : finders) {
finder.scout(type, hudson);
}
@@ -469,7 +336,7 @@ public class ClassicPluginStrategy implements PluginStrategy {
} catch (AbstractMethodError e) {
// backward compatibility
for (T t : finder.findExtensions(type, hudson))
- r.add(new ExtensionComponent(t));
+ r.add(new ExtensionComponent<>(t));
}
}
@@ -662,6 +529,9 @@ public class ClassicPluginStrategy implements PluginStrategy {
z.add(mapper);
z.execute();
}
+ if (classesJar.isFile()) {
+ LOGGER.log(Level.WARNING, "Created {0}; update plugin to a version created with a newer harness", classesJar);
+ }
}
private static void unzipExceptClasses(File archive, File destDir, Project prj) {
@@ -708,7 +578,7 @@ public class ClassicPluginStrategy implements PluginStrategy {
CyclicGraphDetector cgd = new CyclicGraphDetector() {
@Override
protected List getEdges(PluginWrapper pw) {
- List dep = new ArrayList();
+ List dep = new ArrayList<>();
for (Dependency d : pw.getDependencies()) {
PluginWrapper p = pluginManager.getPlugin(d.shortName);
if (p!=null && p.isActive())
@@ -750,19 +620,20 @@ public class ClassicPluginStrategy implements PluginStrategy {
Class> c = ClassLoaderReflectionToolkit._findLoadedClass(pw.classLoader, name);
if (c!=null) return c;
return ClassLoaderReflectionToolkit._findClass(pw.classLoader, name);
- } catch (ClassNotFoundException e) {
+ } catch (ClassNotFoundException ignored) {
//not found. try next
}
}
} else {
for (Dependency dep : dependencies) {
PluginWrapper p = pluginManager.getPlugin(dep.shortName);
- if(p!=null)
+ if(p!=null) {
try {
return p.classLoader.loadClass(name);
- } catch (ClassNotFoundException _) {
- // try next
+ } catch (ClassNotFoundException ignored) {
+ // OK, try next
}
+ }
}
}
@@ -770,8 +641,10 @@ public class ClassicPluginStrategy implements PluginStrategy {
}
@Override
+ @SuppressFBWarnings(value = "DMI_COLLECTION_OF_URLS",
+ justification = "Should not produce network overheads since the URL is local. JENKINS-53793 is a follow-up")
protected Enumeration findResources(String name) throws IOException {
- HashSet result = new HashSet();
+ HashSet result = new HashSet<>();
if (PluginManager.FAST_LOOKUP) {
for (PluginWrapper pw : getTransitiveDependencies()) {
@@ -818,53 +691,11 @@ public class ClassicPluginStrategy implements PluginStrategy {
/**
* {@link AntClassLoader} with a few methods exposed, {@link Closeable} support, and {@link Transformer} support.
*/
- private final class AntClassLoader2 extends AntClassLoader implements Closeable {
- private final Vector pathComponents;
-
+ private final class AntClassLoader2 extends AntWithFindResourceClassLoader implements Closeable {
private AntClassLoader2(ClassLoader parent) {
- super(parent,true);
-
- try {
- Field $pathComponents = AntClassLoader.class.getDeclaredField("pathComponents");
- $pathComponents.setAccessible(true);
- pathComponents = (Vector)$pathComponents.get(this);
- } catch (NoSuchFieldException | IllegalAccessException e) {
- throw new Error(e);
- }
- }
-
-
- public void addPathFiles(Collection paths) throws IOException {
- for (File f : paths)
- addPathFile(f);
- }
-
- public void close() throws IOException {
- cleanup();
- }
-
- /**
- * As of 1.8.0, {@link AntClassLoader} doesn't implement {@link #findResource(String)}
- * in any meaningful way, which breaks fast lookup. Implement it properly.
- */
- @Override
- protected URL findResource(String name) {
- URL url = null;
-
- // try and load from this loader if the parent either didn't find
- // it or wasn't consulted.
- Enumeration e = pathComponents.elements();
- while (e.hasMoreElements() && url == null) {
- File pathComponent = (File) e.nextElement();
- url = getResourceURL(pathComponent, name);
- if (url != null) {
- log("Resource " + name + " loaded from ant loader", Project.MSG_DEBUG);
- }
- }
-
- return url;
+ super(parent, true);
}
-
+
@Override
protected Class defineClassFromData(File container, byte[] classData, String classname) throws IOException {
if (!DISABLE_TRANSFORMER)
@@ -874,6 +705,5 @@ public class ClassicPluginStrategy implements PluginStrategy {
}
public static boolean useAntClassLoader = SystemProperties.getBoolean(ClassicPluginStrategy.class.getName()+".useAntClassLoader");
- private static final Logger LOGGER = Logger.getLogger(ClassicPluginStrategy.class.getName());
public static boolean DISABLE_TRANSFORMER = SystemProperties.getBoolean(ClassicPluginStrategy.class.getName()+".noBytecodeTransformer");
}
diff --git a/core/src/main/java/hudson/CopyOnWrite.java b/core/src/main/java/hudson/CopyOnWrite.java
index efe21357c4405e122651a284c0cb4c30157fce62..891cbf04bb9ffef6d6f657d05eb4708d94fa0f5e 100644
--- a/core/src/main/java/hudson/CopyOnWrite.java
+++ b/core/src/main/java/hudson/CopyOnWrite.java
@@ -43,7 +43,7 @@ import java.lang.annotation.Target;
*
*
* The field marked with this annotation usually needs to be marked as
- * volatile.
+ * {@code volatile}.
*
* @author Kohsuke Kawaguchi
*/
diff --git a/core/src/main/java/hudson/DNSMultiCast.java b/core/src/main/java/hudson/DNSMultiCast.java
index aa71b2e23b2be38ba799c7d3f3742e9442a5c550..fcb3fe33bb05fdffb9bfd7cf54adde74f0e6e51b 100644
--- a/core/src/main/java/hudson/DNSMultiCast.java
+++ b/core/src/main/java/hudson/DNSMultiCast.java
@@ -32,7 +32,7 @@ public class DNSMultiCast implements Closeable {
try {
jmdns = JmDNS.create();
- Map props = new HashMap();
+ Map props = new HashMap<>();
String rootURL = jenkins.getRootUrl();
if (rootURL==null) return null;
diff --git a/core/src/main/java/hudson/DescriptorExtensionList.java b/core/src/main/java/hudson/DescriptorExtensionList.java
index 16d96ae441b0ff5a7cc4a3e59e8398db0cc595a4..5e9d35f7ec4a3c87f0400bd9737361a7dda6cf7e 100644
--- a/core/src/main/java/hudson/DescriptorExtensionList.java
+++ b/core/src/main/java/hudson/DescriptorExtensionList.java
@@ -31,7 +31,6 @@ import jenkins.model.Jenkins;
import hudson.model.ViewDescriptor;
import hudson.model.Descriptor.FormException;
import hudson.util.AdaptedIterator;
-import hudson.util.Memoizer;
import hudson.util.Iterators.FlattenIterator;
import hudson.slaves.NodeDescriptor;
import hudson.tasks.Publisher;
@@ -41,6 +40,8 @@ import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -75,7 +76,7 @@ public class DescriptorExtensionList, D extends Descrip
if (describableType == (Class) Publisher.class) {
return (DescriptorExtensionList) new Publisher.DescriptorExtensionListImpl(jenkins);
}
- return new DescriptorExtensionList(jenkins,describableType);
+ return new DescriptorExtensionList<>(jenkins, describableType);
}
/**
@@ -160,13 +161,13 @@ public class DescriptorExtensionList, D extends Descrip
@Override
public boolean add(D d) {
boolean r = super.add(d);
- hudson.getExtensionList(Descriptor.class).add(d);
+ getDescriptorExtensionList().add(d);
return r;
}
@Override
public boolean remove(Object o) {
- hudson.getExtensionList(Descriptor.class).remove(o);
+ getDescriptorExtensionList().remove(o);
return super.remove(o);
}
@@ -175,7 +176,8 @@ public class DescriptorExtensionList, D extends Descrip
*/
@Override
protected Object getLoadLock() {
- return this;
+ // Get a lock for the singleton extension list to prevent deadlocks (JENKINS-55361)
+ return getDescriptorExtensionList().getLoadLock();
}
/**
@@ -188,7 +190,7 @@ public class DescriptorExtensionList, D extends Descrip
LOGGER.log(Level.WARNING, "Cannot load extension components, because Jenkins instance has not been assigned yet");
return Collections.emptyList();
}
- return _load(jenkins.getExtensionList(Descriptor.class).getComponents());
+ return _load(getDescriptorExtensionList().getComponents());
}
@Override
@@ -197,7 +199,7 @@ public class DescriptorExtensionList, D extends Descrip
}
private List> _load(Iterable> set) {
- List> r = new ArrayList>();
+ List> r = new ArrayList<>();
for( ExtensionComponent c : set ) {
Descriptor d = c.getInstance();
try {
@@ -210,17 +212,19 @@ public class DescriptorExtensionList, D extends Descrip
return r;
}
+ private ExtensionList getDescriptorExtensionList() {
+ return ExtensionList.lookup(Descriptor.class);
+ }
+
/**
* Stores manually registered Descriptor instances. Keyed by the {@link Describable} type.
*/
- private static final Memoizer>> legacyDescriptors = new Memoizer>>() {
- public CopyOnWriteArrayList compute(Class key) {
- return new CopyOnWriteArrayList();
- }
- };
+ @SuppressWarnings("rawtypes")
+ private static final Map>> legacyDescriptors = new ConcurrentHashMap<>();
+ @SuppressWarnings({"unchecked", "rawtypes"})
private static > CopyOnWriteArrayList>> getLegacyDescriptors(Class type) {
- return (CopyOnWriteArrayList)legacyDescriptors.get(type);
+ return legacyDescriptors.computeIfAbsent(type, key -> new CopyOnWriteArrayList());
}
/**
diff --git a/core/src/main/java/hudson/EnvVars.java b/core/src/main/java/hudson/EnvVars.java
index 5cdc672bf4cd8caf70f40d045c8e10a7bb4bb49c..8caaebf31290c23a676ceadc9cee171106b86650 100644
--- a/core/src/main/java/hudson/EnvVars.java
+++ b/core/src/main/java/hudson/EnvVars.java
@@ -43,6 +43,8 @@ import java.util.Arrays;
import java.util.TreeSet;
import java.util.UUID;
import java.util.logging.Logger;
+import javax.annotation.Nonnull;
+import javax.annotation.CheckForNull;
/**
* Environment variables.
@@ -53,7 +55,7 @@ import java.util.logging.Logger;
* but case insensitive way (that is, cmd.exe can get both FOO and foo as environment variables
* when it's launched, and the "set" command will display it accordingly, but "echo %foo%" results in
* echoing the value of "FOO", not "foo" — this is presumably caused by the behavior of the underlying
- * Win32 API GetEnvironmentVariable acting in case insensitive way.) Windows users are also
+ * Win32 API {@code GetEnvironmentVariable} acting in case insensitive way.) Windows users are also
* used to write environment variable case-insensitively (like %Path% vs %PATH%), and you can see many
* documents on the web that claims Windows environment variables are case insensitive.
*
@@ -64,14 +66,15 @@ import java.util.logging.Logger;
*
* In Jenkins, often we need to build up "environment variable overrides"
* on master, then to execute the process on agents. This causes a problem
- * when working with variables like PATH. So to make this work,
- * we introduce a special convention PATH+FOO — all entries
- * that starts with PATH+ are merged and prepended to the inherited
- * PATH variable, on the process where a new process is executed.
+ * when working with variables like {@code PATH}. So to make this work,
+ * we introduce a special convention {@code PATH+FOO} — all entries
+ * that starts with {@code PATH+} are merged and prepended to the inherited
+ * {@code PATH} variable, on the process where a new process is executed.
*
* @author Kohsuke Kawaguchi
*/
public class EnvVars extends TreeMap {
+ private static final long serialVersionUID = 4320331661987259022L;
private static Logger LOGGER = Logger.getLogger(EnvVars.class.getName());
/**
* If this {@link EnvVars} object represents the whole environment variable set,
@@ -83,12 +86,29 @@ public class EnvVars extends TreeMap {
* So this property remembers that information.
*/
private Platform platform;
+
+ /**
+ * Gets the platform for which these env vars targeted.
+ * @since 2.144
+ * @return The platform.
+ */
+ public @CheckForNull Platform getPlatform() {
+ return platform;
+ }
+ /**
+ * Sets the platform for which these env vars target.
+ * @since 2.144
+ * @param platform the platform to set.
+ */
+ public void setPlatform(@Nonnull Platform platform) {
+ this.platform = platform;
+ }
public EnvVars() {
super(CaseInsensitiveComparator.INSTANCE);
}
- public EnvVars(Map m) {
+ public EnvVars(@Nonnull Map m) {
this();
putAll(m);
@@ -100,13 +120,13 @@ public class EnvVars extends TreeMap {
}
}
- public EnvVars(EnvVars m) {
+ public EnvVars(@Nonnull EnvVars m) {
// this constructor is so that in future we can get rid of the downcasting.
this((Map)m);
}
/**
- * Builds an environment variables from an array of the form "key","value","key","value"...
+ * Builds an environment variables from an array of the form {@code "key","value","key","value"...}
*/
public EnvVars(String... keyValuePairs) {
this();
@@ -120,7 +140,7 @@ public class EnvVars extends TreeMap {
* Overrides the current entry by the given entry.
*
*
- * Handles PATH+XYZ notation.
+ * Handles {@code PATH+XYZ} notation.
*/
public void override(String key, String value) {
if(value==null || value.length()==0) {
@@ -180,7 +200,7 @@ public class EnvVars extends TreeMap {
}
public void clear() {
- referredVariables = new TreeSet(comparator);
+ referredVariables = new TreeSet<>(comparator);
}
public String resolve(String name) {
@@ -206,17 +226,19 @@ public class EnvVars extends TreeMap {
}
return refereeSetMap.get(n);
}
- };
-
+ }
+
private final Comparator super String> comparator;
+ @Nonnull
private final EnvVars target;
+ @Nonnull
private final Map overrides;
private Map> refereeSetMap;
private List orderedVariableNames;
- public OverrideOrderCalculator(EnvVars target, Map overrides) {
+ public OverrideOrderCalculator(@Nonnull EnvVars target, @Nonnull Map overrides) {
comparator = target.comparator();
this.target = target;
this.overrides = overrides;
@@ -266,8 +288,8 @@ public class EnvVars extends TreeMap {
* Scan all variables and list all referring variables.
*/
public void scan() {
- refereeSetMap = new TreeMap>(comparator);
- List extendingVariableNames = new ArrayList();
+ refereeSetMap = new TreeMap<>(comparator);
+ List extendingVariableNames = new ArrayList<>();
TraceResolver resolver = new TraceResolver(comparator);
@@ -305,10 +327,10 @@ public class EnvVars extends TreeMap {
// When A refers B, the last appearance of B always comes after
// the last appearance of A.
- List reversedDuplicatedOrder = new ArrayList(sorter.getSorted());
+ List reversedDuplicatedOrder = new ArrayList<>(sorter.getSorted());
Collections.reverse(reversedDuplicatedOrder);
- orderedVariableNames = new ArrayList(overrides.size());
+ orderedVariableNames = new ArrayList<>(overrides.size());
for(String key: reversedDuplicatedOrder) {
if(overrides.containsKey(key) && !orderedVariableNames.contains(key)) {
orderedVariableNames.add(key);
@@ -323,9 +345,9 @@ public class EnvVars extends TreeMap {
/**
* Overrides all values in the map by the given map. Expressions in values will be expanded.
* See {@link #override(String, String)}.
- * @return this
+ * @return {@code this}
*/
- public EnvVars overrideExpandingAll(Map all) {
+ public EnvVars overrideExpandingAll(@Nonnull Map all) {
for (String key : new OverrideOrderCalculator(this, all).getOrderedVariableNames()) {
override(key, expand(all.get(key)));
}
@@ -422,7 +444,7 @@ public class EnvVars extends TreeMap {
*
*
* If you access this field from agents, then this is the environment
- * variable of the agent agent.
+ * variable of the agent.
*/
public static final Map masterEnvVars = initMaster();
diff --git a/core/src/main/java/hudson/ExpressionFactory2.java b/core/src/main/java/hudson/ExpressionFactory2.java
index 277858c933d5691c4f285a51de3cf8d80dc7c7d1..c8704fccfdb8b06bf0c5b63e1a6fac0969e268cf 100644
--- a/core/src/main/java/hudson/ExpressionFactory2.java
+++ b/core/src/main/java/hudson/ExpressionFactory2.java
@@ -168,5 +168,5 @@ final class ExpressionFactory2 implements ExpressionFactory {
*
* @see Functions#getCurrentJellyContext()
*/
- protected static final ThreadLocal CURRENT_CONTEXT = new ThreadLocal();
+ protected static final ThreadLocal CURRENT_CONTEXT = new ThreadLocal<>();
}
diff --git a/core/src/main/java/hudson/ExtensionComponent.java b/core/src/main/java/hudson/ExtensionComponent.java
index 136d52f81ae7b564db0ca7c1738d34bff73a741c..98e03969f3cc631ad458b9f5b276b9979d108728 100644
--- a/core/src/main/java/hudson/ExtensionComponent.java
+++ b/core/src/main/java/hudson/ExtensionComponent.java
@@ -95,9 +95,7 @@ public class ExtensionComponent implements Comparable>
if (this.instance instanceof Descriptor && that.instance instanceof Descriptor) {
try {
return Util.fixNull(((Descriptor)this.instance).getDisplayName()).compareTo(Util.fixNull(((Descriptor)that.instance).getDisplayName()));
- } catch (RuntimeException x) {
- LOG.log(Level.WARNING, null, x);
- } catch (LinkageError x) {
+ } catch (RuntimeException | LinkageError x) {
LOG.log(Level.WARNING, null, x);
}
}
diff --git a/core/src/main/java/hudson/ExtensionFinder.java b/core/src/main/java/hudson/ExtensionFinder.java
index 6208f6cec8322a0145f2fc4436519000d07e0ce6..9e9c18f63b53749471149199d9d5003b41b9b26b 100644
--- a/core/src/main/java/hudson/ExtensionFinder.java
+++ b/core/src/main/java/hudson/ExtensionFinder.java
@@ -23,6 +23,7 @@
*/
package hudson;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@@ -35,8 +36,9 @@ import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.Scope;
import com.google.inject.Scopes;
+import com.google.inject.matcher.Matchers;
import com.google.inject.name.Names;
-import com.google.common.collect.ImmutableList;
+import com.google.inject.spi.ProvisionListener;
import hudson.init.InitMilestone;
import hudson.model.Descriptor;
import hudson.model.Hudson;
@@ -49,20 +51,25 @@ import net.java.sezpoz.Index;
import net.java.sezpoz.IndexItem;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.springframework.util.ClassUtils;
+import javax.annotation.PostConstruct;
import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
-import java.util.logging.Logger;
+import java.util.Set;
import java.util.logging.Level;
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.lang.reflect.AnnotatedElement;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
+import java.util.logging.Logger;
/**
* Discovers the implementations of an extension point.
@@ -254,12 +261,9 @@ public abstract class ExtensionFinder implements ExtensionPoint {
private Map,GuiceExtensionAnnotation>> extensionAnnotations = Maps.newHashMap();
public GuiceFinder() {
- for (ExtensionComponent ec : moduleFinder.find(GuiceExtensionAnnotation.class, Hudson.getInstance())) {
- GuiceExtensionAnnotation gea = ec.getInstance();
- extensionAnnotations.put(gea.annotationType,gea);
- }
+ refreshExtensionAnnotations();
- sezpozIndex = loadSezpozIndices(Jenkins.getInstance().getPluginManager().uberClassLoader);
+ SezpozModule extensions = new SezpozModule(loadSezpozIndices(Jenkins.getInstance().getPluginManager().uberClassLoader));
List modules = new ArrayList<>();
modules.add(new AbstractModule() {
@@ -270,7 +274,7 @@ public abstract class ExtensionFinder implements ExtensionPoint {
bind(PluginManager.class).toInstance(j.getPluginManager());
}
});
- modules.add(new SezpozModule(sezpozIndex));
+ modules.add(extensions);
for (ExtensionComponent ec : moduleFinder.find(Module.class, Hudson.getInstance())) {
modules.add(ec.getInstance());
@@ -278,6 +282,7 @@ public abstract class ExtensionFinder implements ExtensionPoint {
try {
container = Guice.createInjector(modules);
+ sezpozIndex = extensions.getLoadedIndex();
} catch (Throwable e) {
LOGGER.log(Level.SEVERE, "Failed to create Guice container from all the plugins",e);
// failing to load all bindings are disastrous, so recover by creating minimum that works
@@ -293,6 +298,13 @@ public abstract class ExtensionFinder implements ExtensionPoint {
});
}
+ private void refreshExtensionAnnotations() {
+ for (ExtensionComponent ec : moduleFinder.find(GuiceExtensionAnnotation.class, Hudson.getInstance())) {
+ GuiceExtensionAnnotation gea = ec.getInstance();
+ extensionAnnotations.put(gea.annotationType,gea);
+ }
+ }
+
private ImmutableList> loadSezpozIndices(ClassLoader classLoader) {
List> indices = Lists.newArrayList();
for (GuiceExtensionAnnotation> gea : extensionAnnotations.values()) {
@@ -315,17 +327,17 @@ public abstract class ExtensionFinder implements ExtensionPoint {
*/
@Override
public synchronized ExtensionComponentSet refresh() throws ExtensionRefreshException {
+ refreshExtensionAnnotations();
// figure out newly discovered sezpoz components
List> delta = Lists.newArrayList();
for (Class extends Annotation> annotationType : extensionAnnotations.keySet()) {
delta.addAll(Sezpoz.listDelta(annotationType,sezpozIndex));
}
- List> l = Lists.newArrayList(sezpozIndex);
- l.addAll(delta);
- sezpozIndex = l;
+
+ SezpozModule deltaExtensions = new SezpozModule(delta);
List modules = new ArrayList<>();
- modules.add(new SezpozModule(delta));
+ modules.add(deltaExtensions);
for (ExtensionComponent ec : moduleFinder.refresh().find(Module.class)) {
modules.add(ec.getInstance());
}
@@ -333,6 +345,9 @@ public abstract class ExtensionFinder implements ExtensionPoint {
try {
final Injector child = container.createChildInjector(modules);
container = child;
+ List> l = Lists.newArrayList(sezpozIndex);
+ l.addAll(deltaExtensions.getLoadedIndex());
+ sezpozIndex = l;
return new ExtensionComponentSet() {
@Override
@@ -446,11 +461,13 @@ public abstract class ExtensionFinder implements ExtensionPoint {
* Instead of using SezPoz to instantiate, we'll instantiate them by using Guice,
* so that we can take advantage of dependency injection.
*/
- private class SezpozModule extends AbstractModule {
+ private class SezpozModule extends AbstractModule implements ProvisionListener {
private final List> index;
+ private final List> loadedIndex;
public SezpozModule(List> index) {
this.index = index;
+ this.loadedIndex = new ArrayList<>();
}
/**
@@ -493,6 +510,9 @@ public abstract class ExtensionFinder implements ExtensionPoint {
@SuppressWarnings({"unchecked", "ChainOfInstanceofChecks"})
@Override
protected void configure() {
+
+ bindListener(Matchers.any(), this);
+
for (final IndexItem,Object> item : index) {
boolean optional = isOptional(item.annotation());
try {
@@ -527,6 +547,7 @@ public abstract class ExtensionFinder implements ExtensionPoint {
}
}).in(scope);
}
+ loadedIndex.add(item);
} catch (Exception|LinkageError e) {
// sometimes the instantiation fails in an indirect classloading failure,
// which results in a LinkageError
@@ -535,9 +556,68 @@ public abstract class ExtensionFinder implements ExtensionPoint {
}
}
}
+
+ public List> getLoadedIndex() {
+ return Collections.unmodifiableList(loadedIndex);
+ }
+
+ @Override
+ public void onProvision(ProvisionInvocation provision) {
+ final T instance = provision.provision();
+ if (instance == null) return;
+ List methods = new LinkedList<>();
+ Class c = instance.getClass();
+
+ // find PostConstruct methods in class hierarchy, the one from parent class being first in list
+ // so that we invoke them before derived class one. This isn't specified in JSR-250 but implemented
+ // this way in Spring and what most developers would expect to happen.
+
+ final Set interfaces = ClassUtils.getAllInterfacesAsSet(instance);
+
+ while (c != Object.class) {
+ Arrays.stream(c.getDeclaredMethods())
+ .map(m -> getMethodAndInterfaceDeclarations(m, interfaces))
+ .flatMap(Collection::stream)
+ .filter(m -> m.getAnnotation(PostConstruct.class) != null)
+ .findFirst()
+ .ifPresent(method -> methods.add(0, method));
+ c = c.getSuperclass();
+ }
+
+ for (Method postConstruct : methods) {
+ try {
+ postConstruct.setAccessible(true);
+ postConstruct.invoke(instance);
+ } catch (final Exception e) {
+ throw new RuntimeException(String.format("@PostConstruct %s", postConstruct), e);
+ }
+ }
+ }
}
}
+ /**
+ * Returns initial {@link Method} as well as all matching ones found in interfaces.
+ * This allows to introspect metadata for a method which is both declared in parent class and in implemented
+ * interface(s). interfaces typically is obtained by {@link ClassUtils#getAllInterfacesAsSet}
+ */
+ Collection getMethodAndInterfaceDeclarations(Method method, Collection interfaces) {
+ final List methods = new ArrayList<>();
+ methods.add(method);
+
+ // we search for matching method by iteration and comparison vs getMethod to avoid repeated NoSuchMethodException
+ // being thrown, while interface typically only define a few set of methods to check.
+ interfaces.stream()
+ .map(Class::getMethods)
+ .flatMap(Arrays::stream)
+ .filter(m -> m.getName().equals(method.getName()) && Arrays.equals(m.getParameterTypes(), method.getParameterTypes()))
+ .findFirst()
+ .ifPresent(methods::add);
+
+ return methods;
+ }
+
+
/**
* The bootstrap implementation that looks for the {@link Extension} marker.
*
diff --git a/core/src/main/java/hudson/ExtensionList.java b/core/src/main/java/hudson/ExtensionList.java
index f2fcab81d403e63588322b73ceacae33dce66617..c3f408c4777ca3ced9451299c738b7e91e4e2b06 100644
--- a/core/src/main/java/hudson/ExtensionList.java
+++ b/core/src/main/java/hudson/ExtensionList.java
@@ -30,7 +30,6 @@ import jenkins.ExtensionComponentSet;
import jenkins.model.Jenkins;
import hudson.util.AdaptedIterator;
import hudson.util.DescriptorList;
-import hudson.util.Memoizer;
import hudson.util.Iterators;
import hudson.ExtensionPoint.LegacyInstancesAreScopedToHudson;
@@ -40,7 +39,9 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.Vector;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -85,7 +86,7 @@ public class ExtensionList extends AbstractList implements OnMaster {
@CopyOnWrite
private volatile List> extensions;
- private final List listeners = new CopyOnWriteArrayList();
+ private final List listeners = new CopyOnWriteArrayList<>();
/**
* Place to store manually registered instances with the per-Hudson scope.
@@ -103,7 +104,7 @@ public class ExtensionList extends AbstractList implements OnMaster {
}
protected ExtensionList(Jenkins jenkins, Class extensionType) {
- this(jenkins,extensionType,new CopyOnWriteArrayList>());
+ this(jenkins,extensionType, new CopyOnWriteArrayList<>());
}
/**
@@ -144,15 +145,29 @@ public class ExtensionList extends AbstractList implements OnMaster {
* Looks for the extension instance of the given type (subclasses excluded),
* or return null.
*/
- public @CheckForNull U get(Class type) {
+ public @CheckForNull U get(@Nonnull Class type) {
for (T ext : this)
if(ext.getClass()==type)
return type.cast(ext);
return null;
}
+ /**
+ * Looks for the extension instance of the given type (subclasses excluded),
+ * or throws an IllegalStateException.
+ *
+ * Meant to simplify call inside @Extension annotated class to retrieve their own instance.
+ */
+ public @Nonnull U getInstance(@Nonnull Class type) throws IllegalStateException {
+ for (T ext : this)
+ if(ext.getClass()==type)
+ return type.cast(ext);
+
+ throw new IllegalStateException("The class " + type.getName() + " was not found, potentially not yet loaded");
+ }
+
@Override
- public Iterator iterator() {
+ public @Nonnull Iterator iterator() {
// we need to intercept mutation, so for now don't allow Iterator.remove
return new AdaptedIterator,T>(Iterators.readOnly(ensureLoaded().iterator())) {
protected T adapt(ExtensionComponent item) {
@@ -222,7 +237,7 @@ public class ExtensionList extends AbstractList implements OnMaster {
private synchronized boolean removeSync(Object o) {
boolean removed = removeComponent(legacyInstances, o);
if(extensions!=null) {
- List> r = new ArrayList>(extensions);
+ List> r = new ArrayList<>(extensions);
removed |= removeComponent(r,o);
extensions = sort(r);
}
@@ -230,8 +245,7 @@ public class ExtensionList extends AbstractList implements OnMaster {
}
private boolean removeComponent(Collection> collection, Object t) {
- for (Iterator> itr = collection.iterator(); itr.hasNext();) {
- ExtensionComponent c = itr.next();
+ for (ExtensionComponent c : collection) {
if (c.getInstance().equals(t)) {
return collection.remove(c);
}
@@ -265,11 +279,11 @@ public class ExtensionList extends AbstractList implements OnMaster {
}
private synchronized boolean addSync(T t) {
- legacyInstances.add(new ExtensionComponent(t));
+ legacyInstances.add(new ExtensionComponent<>(t));
// if we've already filled extensions, add it
if(extensions!=null) {
- List> r = new ArrayList>(extensions);
- r.add(new ExtensionComponent(t));
+ List> r = new ArrayList<>(extensions);
+ r.add(new ExtensionComponent<>(t));
extensions = sort(r);
}
return true;
@@ -359,8 +373,10 @@ public class ExtensionList extends AbstractList implements OnMaster {
* Loads all the extensions.
*/
protected List> load() {
- if (LOGGER.isLoggable(Level.FINE))
- LOGGER.log(Level.FINE,"Loading ExtensionList: "+extensionType, new Throwable());
+ LOGGER.fine(() -> String.format("Loading ExtensionList '%s'", extensionType.getName()));
+ if (LOGGER.isLoggable(Level.FINER)) {
+ LOGGER.log(Level.FINER, String.format("Loading ExtensionList '%s' from", extensionType.getName()), new Throwable("Only present for stacktrace information"));
+ }
return jenkins.getPluginManager().getPluginStrategy().findComponents(extensionType, hudson);
}
@@ -381,7 +397,7 @@ public class ExtensionList extends AbstractList implements OnMaster {
* The implementation should copy a list, do a sort, and return the new instance.
*/
protected List> sort(List> r) {
- r = new ArrayList>(r);
+ r = new ArrayList<>(r);
Collections.sort(r);
return r;
}
@@ -395,11 +411,12 @@ public class ExtensionList extends AbstractList implements OnMaster {
return create((Jenkins)hudson,type);
}
+ @SuppressWarnings({"unchecked", "rawtypes"})
public static ExtensionList create(Jenkins jenkins, Class type) {
if(type.getAnnotation(LegacyInstancesAreScopedToHudson.class)!=null)
- return new ExtensionList(jenkins,type);
+ return new ExtensionList<>(jenkins, type);
else {
- return new ExtensionList(jenkins,type,staticLegacyInstances.get(type));
+ return new ExtensionList(jenkins, type, staticLegacyInstances.computeIfAbsent(type, key -> new CopyOnWriteArrayList()));
}
}
@@ -418,13 +435,29 @@ public class ExtensionList extends AbstractList implements OnMaster {
}
/**
- * Places to store static-scope legacy instances.
+ * Convenience method allowing lookup of the only instance of a given type.
+ * Equivalent to {@code ExtensionList.lookup(Class).get(Class)} if there is one instance,
+ * and throws an {@code IllegalStateException} otherwise.
+ *
+ * @param type The type to look up.
+ * @return the singleton instance of the given type in its list.
+ * @throws IllegalStateException if there are no instances, or more than one
+ *
+ * @since 2.87
*/
- private static final Memoizer staticLegacyInstances = new Memoizer() {
- public CopyOnWriteArrayList compute(Class key) {
- return new CopyOnWriteArrayList();
+ public static @Nonnull U lookupSingleton(Class type) {
+ ExtensionList all = lookup(type);
+ if (all.size() != 1) {
+ throw new IllegalStateException("Expected 1 instance of " + type.getName() + " but got " + all.size());
}
- };
+ return all.get(0);
+ }
+
+ /**
+ * Places to store static-scope legacy instances.
+ */
+ @SuppressWarnings("rawtypes")
+ private static final Map staticLegacyInstances = new ConcurrentHashMap<>();
/**
* Exposed for the test harness to clear all legacy extension instances.
diff --git a/core/src/main/java/hudson/ExtensionListView.java b/core/src/main/java/hudson/ExtensionListView.java
index 59b81ffd95869297d20ea67694b923652235812d..100dad6fb758520864230cd18157b52bc062986e 100644
--- a/core/src/main/java/hudson/ExtensionListView.java
+++ b/core/src/main/java/hudson/ExtensionListView.java
@@ -23,6 +23,7 @@
*/
package hudson;
+import hudson.tasks.UserNameResolver;
import jenkins.model.Jenkins;
import hudson.util.CopyOnWriteList;
diff --git a/core/src/main/java/hudson/ExtensionPoint.java b/core/src/main/java/hudson/ExtensionPoint.java
index e544ed7576ad411694e868a149dbf2ee1a6971b2..f49a5a096cc7ba2b6671b3ec8a0dd5b23746f3a2 100644
--- a/core/src/main/java/hudson/ExtensionPoint.java
+++ b/core/src/main/java/hudson/ExtensionPoint.java
@@ -29,7 +29,6 @@ import static java.lang.annotation.ElementType.TYPE;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;
-import jenkins.util.io.OnMaster;
/**
* Marker interface that designates extensible components
diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java
index a06681ed19f5b9076d92dc0d80d1860721185bc5..e2a4b30b8cd76c6c68f5018d0161273a540ab540 100644
--- a/core/src/main/java/hudson/FilePath.java
+++ b/core/src/main/java/hudson/FilePath.java
@@ -58,10 +58,11 @@ import hudson.util.IOUtils;
import hudson.util.NamingThreadFactory;
import hudson.util.io.Archiver;
import hudson.util.io.ArchiverFactory;
+
+
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
-import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
@@ -78,10 +79,20 @@ import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
+import java.nio.file.FileSystemException;
+import java.nio.file.FileSystems;
import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.LinkOption;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
+import java.util.EnumSet;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
@@ -106,12 +117,12 @@ import jenkins.SoloFilePathFilter;
import jenkins.model.Jenkins;
import jenkins.security.MasterToSlaveCallable;
import jenkins.util.ContextResettingExecutorService;
-import jenkins.util.SystemProperties;
import jenkins.util.VirtualFile;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.io.input.CountingInputStream;
+import org.apache.commons.lang.StringUtils;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.FileSet;
@@ -121,12 +132,15 @@ import org.jenkinsci.remoting.RoleChecker;
import org.jenkinsci.remoting.RoleSensitive;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
+import org.kohsuke.stapler.Function;
import org.kohsuke.stapler.Stapler;
import static hudson.FilePath.TarCompression.GZIP;
-import static hudson.Util.deleteFile;
+import static hudson.Util.fileToPath;
import static hudson.Util.fixEmpty;
-import static hudson.Util.isSymlink;
+
+import java.util.Collections;
+import org.apache.tools.ant.BuildException;
/**
* {@link File} like object with remoting support.
@@ -168,7 +182,7 @@ import static hudson.Util.isSymlink;
* }
* // if 'file' is on a different node, this FileCallable will
* // be transferred to that node and executed there.
- * private static final class Freshen implements FileCallable<Void> {
+ * private static final class Freshen implements FileCallable<Void> {
* private static final long serialVersionUID = 1;
* @Override public Void invoke(File f, VirtualChannel channel) {
* // f and file represent the same thing
@@ -213,9 +227,14 @@ public final class FilePath implements Serializable {
* This is used to determine whether we are running on the master or the agent.
*/
private transient VirtualChannel channel;
-
- // since the platform of the agent might be different, can't use java.io.File
- private final String remote;
+
+ /**
+ * Represent the path to the file in the master or the agent
+ * Since the platform of the agent might be different, can't use java.io.File
+ *
+ * The field could not be final since it's modified in {@link #readResolve()}
+ */
+ private /*final*/ String remote;
/**
* If this {@link FilePath} is deserialized to handle file access request from a remote computer,
@@ -234,9 +253,9 @@ public final class FilePath implements Serializable {
*
* @param channel
* To create a path that represents a remote path, pass in a {@link Channel}
- * that's connected to that machine. If null, that means the local file path.
+ * that's connected to that machine. If {@code null}, that means the local file path.
*/
- public FilePath(VirtualChannel channel, String remote) {
+ public FilePath(@CheckForNull VirtualChannel channel, @Nonnull String remote) {
this.channel = channel instanceof LocalChannel ? null : channel;
this.remote = normalize(remote);
}
@@ -248,7 +267,7 @@ public final class FilePath implements Serializable {
* A "local" path means a file path on the computer where the
* constructor invocation happened.
*/
- public FilePath(File localPath) {
+ public FilePath(@Nonnull File localPath) {
this.channel = null;
this.remote = normalize(localPath.getPath());
}
@@ -258,12 +277,17 @@ public final class FilePath implements Serializable {
* @param base starting point for resolution, and defines channel
* @param rel a path which if relative will be resolved against base
*/
- public FilePath(FilePath base, String rel) {
+ public FilePath(@Nonnull FilePath base, @Nonnull String rel) {
this.channel = base.channel;
this.remote = normalize(resolvePathIfRelative(base, rel));
}
- private String resolvePathIfRelative(FilePath base, String rel) {
+ private Object readResolve() {
+ this.remote = normalize(this.remote);
+ return this;
+ }
+
+ private String resolvePathIfRelative(@Nonnull FilePath base, @Nonnull String rel) {
if(isAbsolute(rel)) return rel;
if(base.isUnix()) {
// shouldn't need this replace, but better safe than sorry
@@ -278,7 +302,7 @@ public final class FilePath implements Serializable {
/**
* Is the given path name an absolute path?
*/
- private static boolean isAbsolute(String rel) {
+ private static boolean isAbsolute(@Nonnull String rel) {
return rel.startsWith("/") || DRIVE_PATTERN.matcher(rel).matches() || UNC_PATTERN.matcher(rel).matches();
}
@@ -290,7 +314,8 @@ public final class FilePath implements Serializable {
* {@link File#getParent()} etc cannot handle ".." and "." in the path component very well,
* so remove them.
*/
- private static String normalize(String path) {
+ @Restricted(NoExternalUse.class)
+ public static String normalize(@Nonnull String path) {
StringBuilder buf = new StringBuilder();
// Check for prefix designating absolute path
Matcher m = ABSOLUTE_PREFIX_PATTERN.matcher(path);
@@ -300,7 +325,7 @@ public final class FilePath implements Serializable {
}
boolean isAbsolute = buf.length() > 0;
// Split remaining path into tokens, trimming any duplicate or trailing separators
- List tokens = new ArrayList();
+ List tokens = new ArrayList<>();
int s = 0, end = path.length();
for (int i = 0; i < end; i++) {
char c = path.charAt(i);
@@ -361,7 +386,7 @@ public final class FilePath implements Serializable {
return false;
// Windows can handle '/' as a path separator but Unix can't,
// so err on Unix side
- return remote.indexOf("\\")==-1;
+ return !remote.contains("\\");
}
/**
@@ -454,7 +479,18 @@ public final class FilePath implements Serializable {
*/
public int archive(final ArchiverFactory factory, OutputStream os, final DirScanner scanner) throws IOException, InterruptedException {
final OutputStream out = (channel!=null)?new RemoteOutputStream(os):os;
- return act(new SecureFileCallable() {
+ return act(new Archive(factory, out, scanner));
+ }
+ private class Archive extends SecureFileCallable {
+ private final ArchiverFactory factory;
+ private final OutputStream out;
+ private final DirScanner scanner;
+ Archive(ArchiverFactory factory, OutputStream out, DirScanner scanner) {
+ this.factory = factory;
+ this.out = out;
+ this.scanner = scanner;
+ }
+ @Override
public Integer invoke(File f, VirtualChannel channel) throws IOException {
Archiver a = factory.create(out);
try {
@@ -466,7 +502,6 @@ public final class FilePath implements Serializable {
}
private static final long serialVersionUID = 1L;
- });
}
public int archive(final ArchiverFactory factory, OutputStream os, final FileFilter filter) throws IOException, InterruptedException {
@@ -489,26 +524,31 @@ public final class FilePath implements Serializable {
// TODO: post release, re-unite two branches by introducing FileStreamCallable that resolves InputStream
if (this.channel!=target.channel) {// local -> remote or remote->local
final RemoteInputStream in = new RemoteInputStream(read(), Flag.GREEDY);
- target.act(new SecureFileCallable() {
- public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
- unzip(dir, in);
- return null;
- }
-
- private static final long serialVersionUID = 1L;
- });
+ target.act(new UnzipRemote(in));
} else {// local -> local or remote->remote
- target.act(new SecureFileCallable() {
-
- public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
- assert !FilePath.this.isRemote(); // this.channel==target.channel above
- unzip(dir, reading(new File(FilePath.this.getRemote()))); // shortcut to local file
- return null;
- }
-
- private static final long serialVersionUID = 1L;
- });
+ target.act(new UnzipLocal());
+ }
+ }
+ private class UnzipRemote extends SecureFileCallable {
+ private final RemoteInputStream in;
+ UnzipRemote(RemoteInputStream in) {
+ this.in = in;
+ }
+ @Override
+ public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
+ unzip(dir, in);
+ return null;
+ }
+ private static final long serialVersionUID = 1L;
+ }
+ private class UnzipLocal extends SecureFileCallable {
+ @Override
+ public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
+ assert !FilePath.this.isRemote(); // this.channel==target.channel above
+ unzip(dir, reading(new File(FilePath.this.getRemote()))); // shortcut to local file
+ return null;
}
+ private static final long serialVersionUID = 1L;
}
/**
@@ -525,23 +565,36 @@ public final class FilePath implements Serializable {
// TODO: post release, re-unite two branches by introducing FileStreamCallable that resolves InputStream
if (this.channel!=target.channel) {// local -> remote or remote->local
final RemoteInputStream in = new RemoteInputStream(read(), Flag.GREEDY);
- target.act(new SecureFileCallable() {
- public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
- readFromTar(FilePath.this.getName(),dir,compression.extract(in));
- return null;
- }
-
- private static final long serialVersionUID = 1L;
- });
+ target.act(new UntarRemote(compression, in));
} else {// local -> local or remote->remote
- target.act(new SecureFileCallable() {
- public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
- readFromTar(FilePath.this.getName(),dir,compression.extract(FilePath.this.read()));
- return null;
- }
- private static final long serialVersionUID = 1L;
- });
+ target.act(new UntarLocal(compression));
+ }
+ }
+ private class UntarRemote extends SecureFileCallable {
+ private final TarCompression compression;
+ private final RemoteInputStream in;
+ UntarRemote(TarCompression compression, RemoteInputStream in) {
+ this.compression = compression;
+ this.in = in;
+ }
+ @Override
+ public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
+ readFromTar(FilePath.this.getName(), dir, compression.extract(in));
+ return null;
}
+ private static final long serialVersionUID = 1L;
+ }
+ private class UntarLocal extends SecureFileCallable {
+ private final TarCompression compression;
+ UntarLocal(TarCompression compression) {
+ this.compression = compression;
+ }
+ @Override
+ public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException {
+ readFromTar(FilePath.this.getName(), dir, compression.extract(FilePath.this.read()));
+ return null;
+ }
+ private static final long serialVersionUID = 1L;
}
/**
@@ -554,13 +607,19 @@ public final class FilePath implements Serializable {
*/
public void unzipFrom(InputStream _in) throws IOException, InterruptedException {
final InputStream in = new RemoteInputStream(_in, Flag.GREEDY);
- act(new SecureFileCallable() {
- public Void invoke(File dir, VirtualChannel channel) throws IOException {
- unzip(dir, in);
- return null;
- }
- private static final long serialVersionUID = 1L;
- });
+ act(new UnzipFrom(in));
+ }
+ private class UnzipFrom extends SecureFileCallable {
+ private final InputStream in;
+ UnzipFrom(InputStream in) {
+ this.in = in;
+ }
+ @Override
+ public Void invoke(File dir, VirtualChannel channel) throws IOException {
+ unzip(dir, in);
+ return null;
+ }
+ private static final long serialVersionUID = 1L;
}
private void unzip(File dir, InputStream in) throws IOException {
@@ -578,13 +637,16 @@ public final class FilePath implements Serializable {
private void unzip(File dir, File zipFile) throws IOException {
dir = dir.getAbsoluteFile(); // without absolutization, getParentFile below seems to fail
ZipFile zip = new ZipFile(zipFile);
- @SuppressWarnings("unchecked")
Enumeration entries = zip.getEntries();
try {
while (entries.hasMoreElements()) {
ZipEntry e = entries.nextElement();
File f = new File(dir, e.getName());
+ if (!f.getCanonicalPath().startsWith(dir.getCanonicalPath())) {
+ throw new IOException(
+ "Zip " + zipFile.getPath() + " contains illegal file name that breaks out of the target directory: " + e.getName());
+ }
if (e.isDirectory()) {
mkdirs(f);
} else {
@@ -615,12 +677,13 @@ public final class FilePath implements Serializable {
* Absolutizes this {@link FilePath} and returns the new one.
*/
public FilePath absolutize() throws IOException, InterruptedException {
- return new FilePath(channel,act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public String invoke(File f, VirtualChannel channel) throws IOException {
- return f.getAbsolutePath();
- }
- }));
+ return new FilePath(channel, act(new Absolutize()));
+ }
+ private static class Absolutize extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ public String invoke(File f, VirtualChannel channel) throws IOException {
+ return f.getAbsolutePath();
+ }
}
/**
@@ -633,14 +696,22 @@ public final class FilePath implements Serializable {
* @since 1.456
*/
public void symlinkTo(final String target, final TaskListener listener) throws IOException, InterruptedException {
- act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
- symlinking(f);
- Util.createSymlink(f.getParentFile(),target,f.getName(),listener);
- return null;
- }
- });
+ act(new SymlinkTo(target, listener));
+ }
+ private class SymlinkTo extends SecureFileCallable {
+ private final String target;
+ private final TaskListener listener;
+ SymlinkTo(String target, TaskListener listener) {
+ this.target = target;
+ this.listener = listener;
+ }
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
+ symlinking(f);
+ Util.createSymlink(f.getParentFile(), target, f.getName(), listener);
+ return null;
+ }
}
/**
@@ -651,12 +722,14 @@ public final class FilePath implements Serializable {
* @since 1.456
*/
public String readLink() throws IOException, InterruptedException {
- return act(new SecureFileCallable() {
- private static final long serialVersionUID = 1L;
- public String invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
- return Util.resolveSymlink(reading(f));
- }
- });
+ return act(new ReadLink());
+ }
+ private class ReadLink extends SecureFileCallable {
+ private static final long serialVersionUID = 1L;
+ @Override
+ public String invoke(File f, VirtualChannel channel) throws IOException, InterruptedException {
+ return Util.resolveSymlink(reading(f));
+ }
}
@Override
@@ -720,16 +793,24 @@ public final class FilePath implements Serializable {
public void untarFrom(InputStream _in, final TarCompression compression) throws IOException, InterruptedException {
try {
final InputStream in = new RemoteInputStream(_in, Flag.GREEDY);
- act(new SecureFileCallable() {
- public Void invoke(File dir, VirtualChannel channel) throws IOException {
- readFromTar("input stream",dir, compression.extract(in));
- return null;
- }
- private static final long serialVersionUID = 1L;
- });
+ act(new UntarFrom(compression, in));
} finally {
- org.apache.commons.io.IOUtils.closeQuietly(_in);
+ _in.close();
+ }
+ }
+ private class UntarFrom extends SecureFileCallable {
+ private final TarCompression compression;
+ private final InputStream in;
+ UntarFrom(TarCompression compression, InputStream in) {
+ this.compression = compression;
+ this.in = in;
+ }
+ @Override
+ public Void invoke(File dir, VirtualChannel channel) throws IOException {
+ readFromTar("input stream",dir, compression.extract(in));
+ return null;
}
+ private static final long serialVersionUID = 1L;
}
/**
@@ -880,9 +961,10 @@ public final class FilePath implements Serializable {
}
/**
- * Reads the URL on the current VM, and writes all the data to this {@link FilePath}
- * (this is different from resolving URL remotely.)
- *
+ * Reads the URL on the current VM, and streams the data to this file using the Remoting channel.
+ *
This is different from resolving URL remotely.
+ * If you instead wished to open an HTTP(S) URL on the remote side,
+ * prefer {@code RobustHTTPClient.copyFromRemotely}.
* @since 1.293
*/
public void copyFrom(URL url) throws IOException, InterruptedException {
@@ -934,7 +1016,7 @@ public final class FilePath implements Serializable {
/**
* Code that gets executed on the machine where the {@link FilePath} is local.
* Used to act on {@link FilePath}.
- * Warning: implementations must be serializable, so prefer a static nested class to an inner class.
+ * Warning: implementations must be serializable, so prefer a static nested class to an inner class.
*
*
* Subtypes would likely want to extend from either {@link MasterToSlaveCallable}
@@ -979,18 +1061,13 @@ public final class FilePath implements Serializable {
if(channel!=null) {
// run this on a remote system
try {
- DelegatingCallable wrapper = new FileCallableWrapper(callable, cl);
+ DelegatingCallable wrapper = new FileCallableWrapper<>(callable, cl);
for (FileCallableWrapperFactory factory : ExtensionList.lookup(FileCallableWrapperFactory.class)) {
wrapper = factory.wrap(wrapper);
}
return channel.call(wrapper);
} catch (TunneledInterruptedException e) {
throw (InterruptedException)new InterruptedException(e.getMessage()).initCause(e);
- } catch (AbortException e) {
- throw e; // pass through so that the caller can catch it as AbortException
- } catch (IOException e) {
- // wrap it into a new IOException so that we get the caller's stack trace as well.
- throw new IOException("remote file operation failed: " + remote + " at " + channel + ": " + e, e);
}
} else {
// the file is on the local machine.
@@ -1059,7 +1136,7 @@ public final class FilePath implements Serializable {
*/
public Future actAsync(final FileCallable