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 cli Command line interface for Jenkins + + + Medium + + org.powermock @@ -21,18 +26,24 @@ org.powermock - powermock-api-mockito + powermock-api-mockito2 test org.kohsuke access-modifier-annotation - 1.7 + + + org.jenkins-ci + annotation-indexer commons-codec commons-codec - 1.4 + + + commons-io + commons-io ${project.groupId} @@ -47,13 +58,39 @@ org.jvnet.localizer localizer - 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-ci trilead-ssh2 build214-jenkins-1 + + com.google.code.findbugs + annotations + provided + + + commons-lang + commons-lang + @@ -64,17 +101,19 @@ - attached + single package - 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.256 2.5.6.SEC03 - 2.4.8 - - true + 2.4.12 @@ -65,7 +63,7 @@ THE SOFTWARE. org.jenkins-ci version-number - 1.3 + 1.6 org.jenkins-ci @@ -95,6 +93,12 @@ THE SOFTWARE. com.google.inject guice + + + com.google.guava + guava + + @@ -105,7 +109,7 @@ THE SOFTWARE. com.github.jnr jnr-posix - 3.0.1 + 3.0.45 org.kohsuke @@ -115,7 +119,7 @@ THE SOFTWARE. org.jenkins-ci trilead-ssh2 - build217-jenkins-8 + build-217-jenkins-14 org.kohsuke.stapler @@ -173,6 +177,17 @@ THE SOFTWARE. tests test + + 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-ci annotation-indexer - 1.11 org.jenkins-ci bytecode-compatibility-transformer - 1.8 + 2.0-beta-2 org.jenkins-ci task-reactor - 1.4 + 1.5 org.jvnet.localizer localizer - 1.24 + 1.26 antlr @@ -234,6 +248,20 @@ THE SOFTWARE. + + + xpp3 + xpp3 + 1.1.4c + + + net.sf.kxml + kxml2 + 2.3.0 + jfree jfreechart @@ -256,7 +284,6 @@ THE SOFTWARE. commons-lang commons-lang - 2.6 commons-digester @@ -440,11 +467,6 @@ THE SOFTWARE. spring-aop ${spring.version} - - xpp3 - xpp3 - 1.1.4c - junit junit @@ -462,13 +484,13 @@ THE SOFTWARE. org.powermock - powermock-api-mockito + powermock-api-mockito2 test - javax.servlet - jstl - 1.1.0 + javax.servlet.jsp.jstl + javax.servlet.jsp.jstl-api + 1.2.1 org.slf4j @@ -496,7 +518,7 @@ THE SOFTWARE. org.jvnet.winp winp - 1.24 + 1.27 org.jenkins-ci @@ -516,7 +538,7 @@ THE SOFTWARE. net.java.dev.jna jna - 4.2.1 + 4.5.2 org.kohsuke @@ -526,12 +548,12 @@ THE SOFTWARE. org.kohsuke libpam4j - 1.8 + 1.11 - org.jvnet.libzfs + org.kohsuke libzfs - 0.5 + 0.8 com.sun.solaris @@ -541,7 +563,7 @@ THE SOFTWARE. net.java.sezpoz sezpoz - 1.12 + 1.13 org.kohsuke.jinterop @@ -551,8 +573,7 @@ THE SOFTWARE. org.kohsuke.metainf-services metainf-services - 1.4 - provided + 1.8 true @@ -566,29 +587,26 @@ THE SOFTWARE. 1.1 - + commons-codec commons-codec - 1.8 org.kohsuke access-modifier-annotation - 1.4 com.google.code.findbugs annotations - 3.0.0 provided commons-fileupload commons-fileupload - 1.3.1-jenkins-1 + 1.3.1-jenkins-2 @@ -603,6 +621,12 @@ THE SOFTWARE. com.google.guava guava + + + com.google.code.findbugs + jsr305 + + com.google.guava @@ -704,7 +728,6 @@ THE SOFTWARE. org.kohsuke access-modifier-checker - @@ -755,7 +778,7 @@ THE SOFTWARE. com.sun.winsw winsw - 2.0.3 + 2.2.0 bin exe ${project.build.outputDirectory}/windows-service @@ -772,7 +795,6 @@ THE SOFTWARE. 0.5C true -noverify - false @@ -788,25 +810,6 @@ THE SOFTWARE. - - org.codehaus.gmaven - gmaven-plugin - - - - - testCompile - - - - - - org.codehaus.groovy - groovy-all - ${groovy.version} - - - org.codehaus.mojo findbugs-maven-plugin @@ -881,41 +884,5 @@ THE SOFTWARE. true - - - cobertura - - - - org.codehaus.gmaven - gmaven-plugin - - - - - test - - execute - - - - ${project.basedir}/src/build-script - - ${project.basedir}/src/build-script/unitTest.groovy - - - - - - - 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 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 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 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 callable) throws IOException, InterruptedException { try { - DelegatingCallable wrapper = new FileCallableWrapper(callable); + DelegatingCallable wrapper = new FileCallableWrapper<>(callable); for (FileCallableWrapperFactory factory : ExtensionList.lookup(FileCallableWrapperFactory.class)) { wrapper = factory.wrap(wrapper); } @@ -1093,23 +1170,28 @@ public final class FilePath implements Serializable { * @since 1.522 */ public Callable asCallableWith(final FileCallable task) { - return new Callable() { - @Override - public V call() throws IOException { - try { - return act(task); - } catch (InterruptedException e) { - throw (IOException)new InterruptedIOException().initCause(e); - } + return new CallableWith<>(task); + } + private class CallableWith implements Callable { + private final FileCallable task; + CallableWith(FileCallable task) { + this.task = task; + } + @Override + public V call() throws IOException { + try { + return act(task); + } catch (InterruptedException e) { + throw (IOException)new InterruptedIOException().initCause(e); } + } - @Override - public void checkRoles(RoleChecker checker) throws SecurityException { - task.checkRoles(checker); - } + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + task.checkRoles(checker); + } - private static final long serialVersionUID = 1L; - }; + private static final long serialVersionUID = 1L; } /** @@ -1117,12 +1199,14 @@ public final class FilePath implements Serializable { * on which this file is available. */ public URI toURI() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public URI invoke(File f, VirtualChannel channel) { - return f.toURI(); - } - }); + return act(new ToURI()); + } + private static class ToURI extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public URI invoke(File f, VirtualChannel channel) { + return f.toURI(); + } } /** @@ -1155,70 +1239,54 @@ public final class FilePath implements Serializable { * Creates this directory. */ public void mkdirs() throws IOException, InterruptedException { - if(!act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { - if(mkdirs(f) || f.exists()) - return true; // OK + if (!act(new Mkdirs())) { + throw new IOException("Failed to mkdirs: " + remote); + } + } + private class Mkdirs extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + if(mkdirs(f) || f.exists()) + return true; // OK - // following Ant task to avoid possible race condition. - Thread.sleep(10); + // following Ant task to avoid possible race condition. + Thread.sleep(10); - return f.mkdirs() || f.exists(); - } - })) - throw new IOException("Failed to mkdirs: "+remote); + return mkdirs(f) || f.exists(); + } } /** * Deletes this directory, including all its contents recursively. */ public void deleteRecursive() throws IOException, InterruptedException { - act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Void invoke(File f, VirtualChannel channel) throws IOException { - deleteRecursive(deleting(f)); - return null; - } - }); + act(new DeleteRecursive()); + } + private class DeleteRecursive extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + Util.deleteRecursive(fileToPath(f), path -> deleting(path.toFile())); + return null; + } } /** * Deletes all the contents of this directory, but not the directory itself */ public void deleteContents() throws IOException, InterruptedException { - act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Void invoke(File f, VirtualChannel channel) throws IOException { - deleteContentsRecursive(f); - return null; - } - }); + act(new DeleteContents()); } - - private void deleteRecursive(File dir) throws IOException { - if(!isSymlink(dir)) - deleteContentsRecursive(dir); - try { - deleteFile(deleting(dir)); - } catch (IOException e) { - // if some of the child directories are big, it might take long enough to delete that - // it allows others to create new files, causing problems like JENKINS-10113 - // so give it one more attempt before we give up. - if(!isSymlink(dir)) - deleteContentsRecursive(dir); - deleteFile(deleting(dir)); + private class DeleteContents extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + Util.deleteContentsRecursive(fileToPath(f), path -> deleting(path.toFile())); + return null; } } - private void deleteContentsRecursive(File file) throws IOException { - File[] files = file.listFiles(); - if(files==null) - return; // the directory didn't exist in the first place - for (File child : files) - deleteRecursive(child); - } - /** * Gets the file name portion except the extension. * @@ -1304,17 +1372,25 @@ public final class FilePath implements Serializable { */ public FilePath createTempFile(final String prefix, final String suffix) throws IOException, InterruptedException { try { - return new FilePath(this,act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public String invoke(File dir, VirtualChannel channel) throws IOException { - File f = writing(File.createTempFile(prefix, suffix, dir)); - return f.getName(); - } - })); + return new FilePath(this, act(new CreateTempFile(prefix, suffix))); } catch (IOException e) { throw new IOException("Failed to create a temp file on "+remote,e); } } + private class CreateTempFile extends SecureFileCallable { + private final String prefix; + private final String suffix; + CreateTempFile(String prefix, String suffix) { + this.prefix = prefix; + this.suffix = suffix; + } + private static final long serialVersionUID = 1L; + @Override + public String invoke(File dir, VirtualChannel channel) throws IOException { + File f = writing(File.createTempFile(prefix, suffix, dir)); + return f.getName(); + } + } /** * Creates a temporary file in this directory and set the contents to the @@ -1360,30 +1436,42 @@ public final class FilePath implements Serializable { */ public FilePath createTextTempFile(final String prefix, final String suffix, final String contents, final boolean inThisDirectory) throws IOException, InterruptedException { try { - return new FilePath(channel,act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public String invoke(File dir, VirtualChannel channel) throws IOException { - if(!inThisDirectory) - dir = new File(System.getProperty("java.io.tmpdir")); - else - mkdirs(dir); + return new FilePath(channel, act(new CreateTextTempFile(inThisDirectory, prefix, suffix, contents))); + } catch (IOException e) { + throw new IOException("Failed to create a temp file on "+remote,e); + } + } + private final class CreateTextTempFile extends SecureFileCallable { + private static final long serialVersionUID = 1L; + private final boolean inThisDirectory; + private final String prefix; + private final String suffix; + private final String contents; + CreateTextTempFile(boolean inThisDirectory, String prefix, String suffix, String contents) { + this.inThisDirectory = inThisDirectory; + this.prefix = prefix; + this.suffix = suffix; + this.contents = contents; + } + @Override + public String invoke(File dir, VirtualChannel channel) throws IOException { + if(!inThisDirectory) + dir = new File(System.getProperty("java.io.tmpdir")); + else + mkdirs(dir); - File f; - try { - f = creating(File.createTempFile(prefix, suffix, dir)); - } catch (IOException e) { - throw new IOException("Failed to create a temporary directory in "+dir,e); - } + File f; + try { + f = creating(File.createTempFile(prefix, suffix, dir)); + } catch (IOException e) { + throw new IOException("Failed to create a temporary directory in "+dir,e); + } - try (Writer w = new FileWriter(writing(f))) { - w.write(contents); - } + try (Writer w = new FileWriter(writing(f))) { + w.write(contents); + } - return f.getAbsolutePath(); - } - })); - } catch (IOException e) { - throw new IOException("Failed to create a temp file on "+remote,e); + return f.getAbsolutePath(); } } @@ -1399,23 +1487,47 @@ public final class FilePath implements Serializable { * @return * The new FilePath pointing to the temporary directory * @since 1.311 - * @see File#createTempFile(String, String) + * @see Files#createTempDirectory(Path, String, FileAttribute[]) */ public FilePath createTempDir(final String prefix, final String suffix) throws IOException, InterruptedException { try { - return new FilePath(this,act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public String invoke(File dir, VirtualChannel channel) throws IOException { - File f = File.createTempFile(prefix, suffix, dir); - f.delete(); - f.mkdir(); - return f.getName(); - } - })); + String[] s; + if (StringUtils.isBlank(suffix)) { + s = new String[]{prefix, "tmp"}; // see File.createTempFile - tmp is used if suffix is null + } else { + s = new String[]{prefix, suffix}; + } + String name = StringUtils.join(s, "."); + return new FilePath(this, act(new CreateTempDir(name))); } catch (IOException e) { throw new IOException("Failed to create a temp directory on "+remote,e); } } + private class CreateTempDir extends SecureFileCallable { + private final String name; + CreateTempDir(String name) { + this.name = name; + } + private static final long serialVersionUID = 1L; + @Override + public String invoke(File dir, VirtualChannel channel) throws IOException { + + Path tempPath; + final boolean isPosix = FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); + + if (isPosix) { + tempPath = Files.createTempDirectory(Util.fileToPath(dir), name, + PosixFilePermissions.asFileAttribute(EnumSet.allOf(PosixFilePermission.class))); + } else { + tempPath = Files.createTempDirectory(Util.fileToPath(dir), name, new FileAttribute[] {}); + } + + if (tempPath.toFile() == null) { + throw new IOException("Failed to obtain file from path " + dir + " on " + remote); + } + return tempPath.toFile().getName(); + } + } /** * Deletes this file. @@ -1423,26 +1535,30 @@ public final class FilePath implements Serializable { * @return true, for a modicum of compatibility */ public boolean delete() throws IOException, InterruptedException { - act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Void invoke(File f, VirtualChannel channel) throws IOException { - Util.deleteFile(deleting(f)); - return null; - } - }); + act(new Delete()); return true; } + private class Delete extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + Util.deleteFile(deleting(f)); + return null; + } + } /** * Checks if the file exists. */ public boolean exists() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Boolean invoke(File f, VirtualChannel channel) throws IOException { - return stating(f).exists(); - } - }); + return act(new Exists()); + } + private class Exists extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Boolean invoke(File f, VirtualChannel channel) throws IOException { + return stating(f).exists(); + } } /** @@ -1453,12 +1569,14 @@ public final class FilePath implements Serializable { * @see #touch(long) */ public long lastModified() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Long invoke(File f, VirtualChannel channel) throws IOException { - return stating(f).lastModified(); - } - }); + return act(new LastModified()); + } + private class LastModified extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Long invoke(File f, VirtualChannel channel) throws IOException { + return stating(f).lastModified(); + } } /** @@ -1467,21 +1585,39 @@ public final class FilePath implements Serializable { * @since 1.299 */ public void touch(final long timestamp) throws IOException, InterruptedException { - act(new SecureFileCallable() { + act(new Touch(timestamp)); + } + private class Touch extends SecureFileCallable { + private final long timestamp; + Touch(long timestamp) { + this.timestamp = timestamp; + } private static final long serialVersionUID = -5094638816500738429L; + @Override public Void invoke(File f, VirtualChannel channel) throws IOException { - if(!f.exists()) - Files.newOutputStream(creating(f).toPath()).close(); + if(!f.exists()) { + Files.newOutputStream(fileToPath(creating(f))).close(); + } if(!stating(f).setLastModified(timestamp)) throw new IOException("Failed to set the timestamp of "+f+" to "+timestamp); return null; } - }); } private void setLastModifiedIfPossible(final long timestamp) throws IOException, InterruptedException { - String message = act(new SecureFileCallable() { + String message = act(new SetLastModified(timestamp)); + + if (message!=null) { + LOGGER.warning(message); + } + } + private class SetLastModified extends SecureFileCallable { + private final long timestamp; + SetLastModified(long timestamp) { + this.timestamp = timestamp; + } private static final long serialVersionUID = -828220335793641630L; + @Override public String invoke(File f, VirtualChannel channel) throws IOException { if(!writing(f).setLastModified(timestamp)) { if (Functions.isWindows()) { @@ -1494,23 +1630,20 @@ public final class FilePath implements Serializable { } return null; } - }); - - if (message!=null) { - LOGGER.warning(message); - } } /** * Checks if the file is a directory. */ public boolean isDirectory() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Boolean invoke(File f, VirtualChannel channel) throws IOException { - return stating(f).isDirectory(); - } - }); + return act(new IsDirectory()); + } + private final class IsDirectory extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Boolean invoke(File f, VirtualChannel channel) throws IOException { + return stating(f).isDirectory(); + } } /** @@ -1519,12 +1652,14 @@ public final class FilePath implements Serializable { * @since 1.129 */ public long length() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Long invoke(File f, VirtualChannel channel) throws IOException { - return stating(f).length(); - } - }); + return act(new Length()); + } + private class Length extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Long invoke(File f, VirtualChannel channel) throws IOException { + return stating(f).length(); + } } /** @@ -1532,12 +1667,14 @@ public final class FilePath implements Serializable { * @since 1.542 */ public long getFreeDiskSpace() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - @Override public Long invoke(File f, VirtualChannel channel) throws IOException { - return f.getFreeSpace(); - } - }); + return act(new GetFreeDiskSpace()); + } + private static class GetFreeDiskSpace extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Long invoke(File f, VirtualChannel channel) throws IOException { + return f.getFreeSpace(); + } } /** @@ -1545,12 +1682,14 @@ public final class FilePath implements Serializable { * @since 1.542 */ public long getTotalDiskSpace() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - @Override public Long invoke(File f, VirtualChannel channel) throws IOException { - return f.getTotalSpace(); - } - }); + return act(new GetTotalDiskSpace()); + } + private static class GetTotalDiskSpace extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Long invoke(File f, VirtualChannel channel) throws IOException { + return f.getTotalSpace(); + } } /** @@ -1558,12 +1697,14 @@ public final class FilePath implements Serializable { * @since 1.542 */ public long getUsableDiskSpace() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - @Override public Long invoke(File f, VirtualChannel channel) throws IOException { - return f.getUsableSpace(); - } - }); + return act(new GetUsableDiskSpace()); + } + private static class GetUsableDiskSpace extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Long invoke(File f, VirtualChannel channel) throws IOException { + return f.getUsableSpace(); + } } /** @@ -1577,32 +1718,46 @@ public final class FilePath implements Serializable { *

* please note mask is expected to be an octal if you use chmod command line values, * so preceded by a '0' in java notation, ie chmod(0644) + *

+ * Only supports setting read, write, or execute permissions for the + * owner, group, or others, so the largest permissible value is 0777. + * Attempting to set larger values (i.e. the setgid, setuid, or sticky + * bits) will cause an IOException to be thrown. * * @since 1.303 * @see #mode() */ public void chmod(final int mask) throws IOException, InterruptedException { if(!isUnix() || mask==-1) return; - act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Void invoke(File f, VirtualChannel channel) throws IOException { - // TODO first check for Java 7+ and use PosixFileAttributeView - _chmod(writing(f), mask); + act(new Chmod(mask)); + } + private class Chmod extends SecureFileCallable { + private static final long serialVersionUID = 1L; + private final int mask; + Chmod(int mask) { + this.mask = mask; + } + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + _chmod(writing(f), mask); - return null; - } - }); + return null; + } } /** - * Run chmod via jnr-posix + * Change permissions via NIO. */ private static void _chmod(File f, int mask) throws IOException { // TODO WindowsPosix actually does something here (WindowsLibC._wchmod); should we let it? // Anyway the existing calls already skip this method if on Windows. if (File.pathSeparatorChar==';') return; // noop - PosixAPI.jnr().chmod(f.getAbsolutePath(),mask); + if (Util.NATIVE_CHMOD_MODE) { + PosixAPI.jnr().chmod(f.getAbsolutePath(), mask); + } else { + Files.setPosixFilePermissions(fileToPath(f), Util.modeToPermissions(mask)); + } } private static boolean CHMOD_WARNED = false; @@ -1617,12 +1772,14 @@ public final class FilePath implements Serializable { */ public int mode() throws IOException, InterruptedException, PosixException { if(!isUnix()) return -1; - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Integer invoke(File f, VirtualChannel channel) throws IOException { - return IOUtils.mode(stating(f)); - } - }); + return act(new Mode()); + } + private class Mode extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public Integer invoke(File f, VirtualChannel channel) throws IOException { + return IOUtils.mode(stating(f)); + } } /** @@ -1631,6 +1788,7 @@ public final class FilePath implements Serializable { *

* This method returns direct children of the directory denoted by the 'this' object. */ + @Nonnull public List list() throws IOException, InterruptedException { return list((FileFilter)null); } @@ -1640,6 +1798,7 @@ public final class FilePath implements Serializable { * * @return can be empty but never null. Doesn't contain "." and ".." */ + @Nonnull public List listDirectories() throws IOException, InterruptedException { return list(new DirectoryFilter()); } @@ -1660,23 +1819,32 @@ public final class FilePath implements Serializable { * If this {@link FilePath} represents a remote path, * the filter object will be executed on the remote machine. */ + @Nonnull public List list(final FileFilter filter) throws IOException, InterruptedException { if (filter != null && !(filter instanceof Serializable)) { throw new IllegalArgumentException("Non-serializable filter of " + filter.getClass()); } - return act(new SecureFileCallable>() { + return act(new ListFilter(filter), (filter != null ? filter : this).getClass().getClassLoader()); + } + private class ListFilter extends SecureFileCallable> { + private final FileFilter filter; + ListFilter(FileFilter filter) { + this.filter = filter; + } private static final long serialVersionUID = 1L; + @Override public List invoke(File f, VirtualChannel channel) throws IOException { File[] children = reading(f).listFiles(filter); - if(children ==null) return null; + if (children == null) { + return Collections.emptyList(); + } - ArrayList r = new ArrayList(children.length); + ArrayList r = new ArrayList<>(children.length); for (File child : children) r.add(new FilePath(child)); return r; } - }, (filter!=null?filter:this).getClass().getClassLoader()); } /** @@ -1687,6 +1855,7 @@ public final class FilePath implements Serializable { * @return * can be empty but always non-null. */ + @Nonnull public FilePath[] list(final String includes) throws IOException, InterruptedException { return list(includes, null); } @@ -1701,6 +1870,7 @@ public final class FilePath implements Serializable { * can be empty but always non-null. * @since 1.407 */ + @Nonnull public FilePath[] list(final String includes, final String excludes) throws IOException, InterruptedException { return list(includes, excludes, true); } @@ -1716,9 +1886,21 @@ public final class FilePath implements Serializable { * can be empty but always non-null. * @since 1.465 */ + @Nonnull public FilePath[] list(final String includes, final String excludes, final boolean defaultExcludes) throws IOException, InterruptedException { - return act(new SecureFileCallable() { + return act(new ListGlob(includes, excludes, defaultExcludes)); + } + private class ListGlob extends SecureFileCallable { + private final String includes; + private final String excludes; + private final boolean defaultExcludes; + ListGlob(String includes, String excludes, boolean defaultExcludes) { + this.includes = includes; + this.excludes = excludes; + this.defaultExcludes = defaultExcludes; + } private static final long serialVersionUID = 1L; + @Override public FilePath[] invoke(File f, VirtualChannel channel) throws IOException { String[] files = glob(reading(f), includes, excludes, defaultExcludes); @@ -1728,7 +1910,6 @@ public final class FilePath implements Serializable { return r; } - }); } /** @@ -1737,45 +1918,51 @@ public final class FilePath implements Serializable { * @return * A set of relative file names from the base directory. */ + @Nonnull private static String[] glob(File dir, String includes, String excludes, boolean defaultExcludes) throws IOException { if(isAbsolute(includes)) throw new IOException("Expecting Ant GLOB pattern, but saw '"+includes+"'. See http://ant.apache.org/manual/Types/fileset.html for syntax"); FileSet fs = Util.createFileSet(dir,includes,excludes); fs.setDefaultexcludes(defaultExcludes); - DirectoryScanner ds = fs.getDirectoryScanner(new Project()); - String[] files = ds.getIncludedFiles(); - return files; + DirectoryScanner ds; + try { + ds = fs.getDirectoryScanner(new Project()); + } catch (BuildException x) { + throw new IOException(x.getMessage()); + } + return ds.getIncludedFiles(); } /** * Reads this file. */ public InputStream read() throws IOException, InterruptedException { - if(channel==null) - return Files.newInputStream(reading(new File(remote)).toPath()); + if(channel==null) { + return Files.newInputStream(fileToPath(reading(new File(remote)))); + } final Pipe p = Pipe.createRemoteToLocal(); - actAsync(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - - @Override - public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { - InputStream fis = null; - try { - fis = Files.newInputStream(reading(f).toPath()); - Util.copyStream(fis, p.getOut()); - } catch (Exception x) { - p.error(x); - } finally { - org.apache.commons.io.IOUtils.closeQuietly(fis); - org.apache.commons.io.IOUtils.closeQuietly(p.getOut()); - } - return null; - } - }); + actAsync(new Read(p)); return p.getIn(); } + private class Read extends SecureFileCallable { + private static final long serialVersionUID = 1L; + private final Pipe p; + Read(Pipe p) { + this.p = p; + } + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + try (InputStream fis = Files.newInputStream(fileToPath(reading(f))); + OutputStream out = p.getOut()) { + org.apache.commons.io.IOUtils.copy(fis, out); + } catch (Exception x) { + p.error(x); + } + return null; + } + } /** * Reads this file from the specific offset. @@ -1822,10 +2009,9 @@ public final class FilePath implements Serializable { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { - final OutputStream out = new java.util.zip.GZIPOutputStream(p.getOut(), 8192); - RandomAccessFile raf = null; - try { - raf = new RandomAccessFile(reading(f), "r"); + try (OutputStream os = p.getOut(); + OutputStream out = new java.util.zip.GZIPOutputStream(os, 8192); + RandomAccessFile raf = new RandomAccessFile(reading(f), "r")) { raf.seek(offset); byte[] buf = new byte[8192]; int len; @@ -1833,15 +2019,6 @@ public final class FilePath implements Serializable { out.write(buf, 0, len); } return null; - } finally { - IOUtils.closeQuietly(out); - if (raf != null) { - try { - raf.close(); - } catch (IOException e) { - // ignore - } - } } } }); @@ -1850,11 +2027,16 @@ public final class FilePath implements Serializable { } /** - * Reads this file into a string, by using the current system encoding. + * Reads this file into a string, by using the current system encoding on the remote machine. */ public String readToString() throws IOException, InterruptedException { - try (InputStream in = read()) { - return org.apache.commons.io.IOUtils.toString(in); + return act(new ReadToString()); + } + private final class ReadToString extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public String invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + return new String(Files.readAllBytes(fileToPath(reading(f)))); } } @@ -1877,39 +2059,48 @@ public final class FilePath implements Serializable { if(channel==null) { File f = new File(remote).getAbsoluteFile(); mkdirs(f.getParentFile()); - return Files.newOutputStream(writing(f).toPath()); + return Files.newOutputStream(fileToPath(writing(f))); } - return act(new SecureFileCallable() { + return act(new WritePipe()); + } + private class WritePipe extends SecureFileCallable { private static final long serialVersionUID = 1L; + @Override public OutputStream invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { f = f.getAbsoluteFile(); mkdirs(f.getParentFile()); - OutputStream fos = Files.newOutputStream(writing(f).toPath()); - return new RemoteOutputStream(fos); + return new RemoteOutputStream(Files.newOutputStream(fileToPath(writing(f)))); } - }); } /** * Overwrites this file by placing the given String as the content. * * @param encoding - * Null to use the platform default encoding. + * Null to use the platform default encoding on the remote machine. * @since 1.105 */ public void write(final String content, final String encoding) throws IOException, InterruptedException { - act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Void invoke(File f, VirtualChannel channel) throws IOException { - mkdirs(f.getParentFile()); - try (OutputStream fos = Files.newOutputStream(writing(f).toPath()); - Writer w = encoding != null ? new OutputStreamWriter(fos, encoding) : new OutputStreamWriter(fos)) { - w.write(content); - } - return null; + act(new Write(encoding, content)); + } + private class Write extends SecureFileCallable { + private static final long serialVersionUID = 1L; + private final String encoding; + private final String content; + Write(String encoding, String content) { + this.encoding = encoding; + this.content = content; + } + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + mkdirs(f.getParentFile()); + try (OutputStream fos = Files.newOutputStream(fileToPath(writing(f))); + Writer w = encoding != null ? new OutputStreamWriter(fos, encoding) : new OutputStreamWriter(fos)) { + w.write(content); } - }); + return null; + } } /** @@ -1917,12 +2108,14 @@ public final class FilePath implements Serializable { * @see Util#getDigestOf(File) */ public String digest() throws IOException, InterruptedException { - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public String invoke(File f, VirtualChannel channel) throws IOException { - return Util.getDigestOf(reading(f)); - } - }); + return act(new Digest()); + } + private class Digest extends SecureFileCallable { + private static final long serialVersionUID = 1L; + @Override + public String invoke(File f, VirtualChannel channel) throws IOException { + return Util.getDigestOf(reading(f)); + } } /** @@ -1933,13 +2126,19 @@ public final class FilePath implements Serializable { if(this.channel != target.channel) { throw new IOException("renameTo target must be on the same host"); } - act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Void invoke(File f, VirtualChannel channel) throws IOException { - reading(f).renameTo(creating(new File(target.remote))); - return null; - } - }); + act(new RenameTo(target)); + } + private class RenameTo extends SecureFileCallable { + private final FilePath target; + RenameTo(FilePath target) { + this.target = target; + } + private static final long serialVersionUID = 1L; + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + Files.move(fileToPath(reading(f)), fileToPath(creating(new File(target.remote))), LinkOption.NOFOLLOW_LINKS); + return null; + } } /** @@ -1951,8 +2150,15 @@ public final class FilePath implements Serializable { if(this.channel != target.channel) { throw new IOException("pullUpTo target must be on the same host"); } - act(new SecureFileCallable() { + act(new MoveAllChildrenTo(target)); + } + private class MoveAllChildrenTo extends SecureFileCallable { + private final FilePath target; + MoveAllChildrenTo(FilePath target) { + this.target = target; + } private static final long serialVersionUID = 1L; + @Override public Void invoke(File f, VirtualChannel channel) throws IOException { // JENKINS-16846: if f.getName() is the same as one of the files/directories in f, // then the rename op will fail @@ -1961,7 +2167,7 @@ public final class FilePath implements Serializable { throw new IOException("Failed to rename "+f+" to "+tmp); File t = new File(target.getRemote()); - + for(File child : reading(tmp).listFiles()) { File target = new File(t, child.getName()); if(!stating(child).renameTo(creating(target))) @@ -1970,7 +2176,6 @@ public final class FilePath implements Serializable { deleting(tmp).delete(); return null; } - }); } /** @@ -1991,11 +2196,32 @@ public final class FilePath implements Serializable { * @since 1.311 */ public void copyToWithPermission(FilePath target) throws IOException, InterruptedException { + // Use NIO copy with StandardCopyOption.COPY_ATTRIBUTES when copying on the same machine. + if (this.channel == target.channel) { + act(new CopyToWithPermission(target)); + return; + } + copyTo(target); // copy file permission target.chmod(mode()); target.setLastModifiedIfPossible(lastModified()); } + private class CopyToWithPermission extends SecureFileCallable { + private final FilePath target; + CopyToWithPermission(FilePath target) { + this.target = target; + } + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + File targetFile = new File(target.remote); + File targetDir = targetFile.getParentFile(); + filterNonNull().mkdirs(targetDir); + Files.createDirectories(fileToPath(targetDir)); + Files.copy(fileToPath(reading(f)), fileToPath(writing(targetFile)), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); + return null; + } + } /** * Sends the contents of this file into the given {@link OutputStream}. @@ -2003,29 +2229,32 @@ public final class FilePath implements Serializable { public void copyTo(OutputStream os) throws IOException, InterruptedException { final OutputStream out = new RemoteOutputStream(os); - act(new SecureFileCallable() { - private static final long serialVersionUID = 4088559042349254141L; - public Void invoke(File f, VirtualChannel channel) throws IOException { - InputStream fis = null; - try { - fis = Files.newInputStream(reading(f).toPath()); - Util.copyStream(fis,out); - return null; - } finally { - org.apache.commons.io.IOUtils.closeQuietly(fis); - org.apache.commons.io.IOUtils.closeQuietly(out); - } - } - }); + act(new CopyTo(out)); // make sure the writes fully got delivered to 'os' before we return. // this is needed because I/O operation is asynchronous syncIO(); } + private class CopyTo extends SecureFileCallable { + private static final long serialVersionUID = 4088559042349254141L; + private final OutputStream out; + CopyTo(OutputStream out) { + this.out = out; + } + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + try (InputStream fis = Files.newInputStream(fileToPath(reading(f)))) { + org.apache.commons.io.IOUtils.copy(fis, out); + return null; + } finally { + out.close(); + } + } + } /** * With fix to JENKINS-11251 (remoting 2.15), this is no longer necessary. - * But I'm keeping it for a while so that users who manually deploy slave.jar has time to deploy new version + * But I'm keeping it for a while so that users who manually deploy agent.jar has time to deploy new version * before this goes away. */ private void syncIO() throws InterruptedException { @@ -2033,10 +2262,10 @@ public final class FilePath implements Serializable { if (channel!=null) channel.syncLocalIO(); } catch (AbstractMethodError e) { - // legacy slave.jar. Handle this gracefully + // legacy agent.jar. Handle this gracefully try { - LOGGER.log(Level.WARNING,"Looks like an old slave.jar. Please update "+ Which.jarFile(Channel.class)+" to the new version",e); - } catch (IOException _) { + LOGGER.log(Level.WARNING,"Looks like an old agent.jar. Please update "+ Which.jarFile(Channel.class)+" to the new version",e); + } catch (IOException ignored) { // really ignore this time } } @@ -2119,83 +2348,26 @@ public final class FilePath implements Serializable { public int copyRecursiveTo(final DirScanner scanner, final FilePath target, final String description) throws IOException, InterruptedException { if(this.channel==target.channel) { // local to local copy. - return act(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Integer invoke(File base, VirtualChannel channel) throws IOException { - if(!base.exists()) return 0; - assert target.channel==null; - final File dest = new File(target.remote); - final AtomicInteger count = new AtomicInteger(); - scanner.scan(base, reading(new FileVisitor() { - @Override - public void visit(File f, String relativePath) throws IOException { - if (f.isFile()) { - File target = new File(dest, relativePath); - mkdirsE(target.getParentFile()); - Util.copyFile(f, writing(target)); - count.incrementAndGet(); - } - } - - @Override - public boolean understandsSymlink() { - return true; - } - - @Override - public void visitSymlink(File link, String target, String relativePath) throws IOException { - try { - mkdirsE(new File(dest, relativePath).getParentFile()); - writing(new File(dest, target)); - Util.createSymlink(dest, target, relativePath, TaskListener.NULL); - } catch (InterruptedException x) { - throw (IOException) new IOException(x.toString()).initCause(x); - } - count.incrementAndGet(); - } - })); - return count.get(); - } - }); + return act(new CopyRecursiveLocal(target, scanner)); } else if(this.channel==null) { // local -> remote copy final Pipe pipe = Pipe.createLocalToRemote(); - Future future = target.actAsync(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Void invoke(File f, VirtualChannel channel) throws IOException { - try (InputStream in = pipe.getIn()) { - readFromTar(remote + '/' + description, f,TarCompression.GZIP.extract(in)); - return null; - } - } - }); - Future future2 = actAsync(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - @Override public Integer invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { - return writeToTar(new File(remote), scanner, TarCompression.GZIP.compress(pipe.getOut())); - } - }); + Future future = target.actAsync(new ReadToTar(pipe, description)); + Future future2 = actAsync(new WriteToTar(scanner, pipe)); try { // JENKINS-9540 in case the reading side failed, report that error first future.get(); return future2.get(); } catch (ExecutionException e) { - throw new IOException(e); + throw ioWithCause(e); } } else { // remote -> local copy final Pipe pipe = Pipe.createRemoteToLocal(); - Future future = actAsync(new SecureFileCallable() { - private static final long serialVersionUID = 1L; - public Integer invoke(File f, VirtualChannel channel) throws IOException { - try (OutputStream out = pipe.getOut()) { - return writeToTar(f, scanner, TarCompression.GZIP.compress(out)); - } - } - }); + Future future = actAsync(new CopyRecursiveRemoteToLocal(pipe, scanner)); try { readFromTar(remote + '/' + description,new File(target.remote),TarCompression.GZIP.extract(pipe.getIn())); } catch (IOException e) {// BuildException or IOException @@ -2204,20 +2376,141 @@ public final class FilePath implements Serializable { throw e; // the remote side completed successfully, so the error must be local } catch (ExecutionException x) { // report both errors - throw new IOException(Functions.printThrowable(e),x); - } catch (TimeoutException _) { - // remote is hanging + e.addSuppressed(x); + throw e; + } catch (TimeoutException ignored) { + // remote is hanging, just throw the original exception throw e; } } try { return future.get(); } catch (ExecutionException e) { - throw new IOException(e); + throw ioWithCause(e); } } } + private IOException ioWithCause(ExecutionException e) { + Throwable cause = e.getCause(); + if (cause == null) cause = e; + return cause instanceof IOException + ? (IOException) cause + : new IOException(cause) + ; + } + + private class CopyRecursiveLocal extends SecureFileCallable { + private final FilePath target; + private final DirScanner scanner; + CopyRecursiveLocal(FilePath target, DirScanner scanner) { + this.target = target; + this.scanner = scanner; + } + private static final long serialVersionUID = 1L; + @Override + public Integer invoke(File base, VirtualChannel channel) throws IOException { + if (!base.exists()) { + return 0; + } + assert target.channel == null; + final File dest = new File(target.remote); + final AtomicInteger count = new AtomicInteger(); + scanner.scan(base, reading(new FileVisitor() { + private boolean exceptionEncountered; + private boolean logMessageShown; + @Override + public void visit(File f, String relativePath) throws IOException { + if (f.isFile()) { + File target = new File(dest, relativePath); + mkdirsE(target.getParentFile()); + Path targetPath = fileToPath(writing(target)); + exceptionEncountered = exceptionEncountered || !tryCopyWithAttributes(f, targetPath); + if (exceptionEncountered) { + Files.copy(fileToPath(f), targetPath, StandardCopyOption.REPLACE_EXISTING); + if (!logMessageShown) { + LOGGER.log(Level.INFO, + "JENKINS-52325: Jenkins failed to retain attributes when copying to {0}, so proceeding without attributes.", + dest.getAbsolutePath()); + logMessageShown = true; + } + } + count.incrementAndGet(); + } + } + private boolean tryCopyWithAttributes(File f, Path targetPath) { + try { + Files.copy(fileToPath(f), targetPath, + StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Unable to copy: {0}", e.getMessage()); + return false; + } + return true; + } + @Override + public boolean understandsSymlink() { + return true; + } + @Override + public void visitSymlink(File link, String target, String relativePath) throws IOException { + try { + mkdirsE(new File(dest, relativePath).getParentFile()); + writing(new File(dest, target)); + Util.createSymlink(dest, target, relativePath, TaskListener.NULL); + } catch (InterruptedException x) { + throw new IOException(x); + } + count.incrementAndGet(); + } + })); + return count.get(); + } + } + private class ReadToTar extends SecureFileCallable { + private final Pipe pipe; + private final String description; + ReadToTar(Pipe pipe, String description) { + this.pipe = pipe; + this.description = description; + } + private static final long serialVersionUID = 1L; + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException { + try (InputStream in = pipe.getIn()) { + readFromTar(remote + '/' + description, f, TarCompression.GZIP.extract(in)); + return null; + } + } + } + private class WriteToTar extends SecureFileCallable { + private final DirScanner scanner; + private final Pipe pipe; + WriteToTar(DirScanner scanner, Pipe pipe) { + this.scanner = scanner; + this.pipe = pipe; + } + private static final long serialVersionUID = 1L; + @Override + public Integer invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + return writeToTar(new File(remote), scanner, TarCompression.GZIP.compress(pipe.getOut())); + } + } + private class CopyRecursiveRemoteToLocal extends SecureFileCallable { + private static final long serialVersionUID = 1L; + private final Pipe pipe; + private final DirScanner scanner; + CopyRecursiveRemoteToLocal(Pipe pipe, DirScanner scanner) { + this.pipe = pipe; + this.scanner = scanner; + } + @Override + public Integer invoke(File f, VirtualChannel channel) throws IOException { + try (OutputStream out = pipe.getOut()) { + return writeToTar(f, scanner, TarCompression.GZIP.compress(out)); + } + } + } /** * Writes files in 'this' directory to a tar stream. @@ -2267,6 +2560,10 @@ public final class FilePath implements Serializable { TarArchiveEntry te; while ((te = t.getNextTarEntry()) != null) { File f = new File(baseDir, te.getName()); + if (!f.toPath().normalize().startsWith(baseDir.toPath())) { + throw new IOException( + "Tar " + name + " contains illegal file name that breaks out of the target directory: " + te.getName()); + } if (te.isDirectory()) { mkdirs(f); } else { @@ -2307,6 +2604,7 @@ public final class FilePath implements Serializable { } private static final class IsUnix extends MasterToSlaveCallable { + @Nonnull public Boolean call() throws IOException { return File.pathSeparatorChar==':'; } @@ -2332,7 +2630,7 @@ public final class FilePath implements Serializable { } /** - * Same as {@link #validateFileMask(String, int, boolean)} with caseSensitive set to true + * Same as {@link #validateAntFileMask(String, int, boolean)} with caseSensitive set to true */ public String validateAntFileMask(final String fileMasks, final int bound) throws IOException, InterruptedException { return validateAntFileMask(fileMasks, bound, true); @@ -2342,7 +2640,7 @@ public final class FilePath implements Serializable { * Default bound for {@link #validateAntFileMask(String, int, boolean)}. * @since 1.592 */ - public static int VALIDATE_ANT_FILE_MASK_BOUND = SystemProperties.getInteger(FilePath.class.getName() + ".VALIDATE_ANT_FILE_MASK_BOUND", 10000); + public static int VALIDATE_ANT_FILE_MASK_BOUND = Integer.getInteger(FilePath.class.getName() + ".VALIDATE_ANT_FILE_MASK_BOUND", 10000); /** * Like {@link #validateAntFileMask(String)} but performing only a bounded number of operations. @@ -2357,9 +2655,20 @@ public final class FilePath implements Serializable { * @throws InterruptedException not only in case of a channel failure, but also if too many operations were performed without finding any matches * @since 1.484 */ - public String validateAntFileMask(final String fileMasks, final int bound, final boolean caseSensitive) throws IOException, InterruptedException { - return act(new MasterToSlaveFileCallable() { + public @CheckForNull String validateAntFileMask(final String fileMasks, final int bound, final boolean caseSensitive) throws IOException, InterruptedException { + return act(new ValidateAntFileMask(fileMasks, caseSensitive, bound)); + } + private class ValidateAntFileMask extends MasterToSlaveFileCallable { + private final String fileMasks; + private final boolean caseSensitive; + private final int bound; + ValidateAntFileMask(String fileMasks, boolean caseSensitive, int bound) { + this.fileMasks = fileMasks; + this.caseSensitive = caseSensitive; + this.bound = bound; + } private static final long serialVersionUID = 1; + @Override public String invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { if(fileMasks.startsWith("~")) return Messages.FilePath_TildaDoesntWork(); @@ -2505,7 +2814,6 @@ public final class FilePath implements Serializable { if(idx2==-1) return idx1; return Math.min(idx1,idx2); } - }); } private static final UrlFactory DEFAULT_URL_FACTORY = new UrlFactory(); @@ -2541,7 +2849,7 @@ public final class FilePath implements Serializable { } /** - * Shortcut for {@link #validateFileMask(String,true,boolean)} as the left-hand side can be null. + * Shortcut for {@link #validateFileMask(String,boolean,boolean)} with {@code errorIfNotExist} true, as the left-hand side can be null. */ public static FormValidation validateFileMask(@CheckForNull FilePath path, String value, boolean caseSensitive) throws IOException { if(path==null) return FormValidation.ok(); @@ -2747,6 +3055,11 @@ public final class FilePath implements Serializable { return classLoader; } + @Override + public String toString() { + return callable.toString(); + } + private static final long serialVersionUID = 1L; } @@ -2771,11 +3084,13 @@ public final class FilePath implements Serializable { * (User's home directory in the Unix sense) of the given channel. */ public static FilePath getHomeDirectory(VirtualChannel ch) throws InterruptedException, IOException { - return ch.call(new MasterToSlaveCallable() { - public FilePath call() throws IOException { - return new FilePath(new File(System.getProperty("user.home"))); - } - }); + return ch.call(new GetHomeDirectory()); + } + private static class GetHomeDirectory extends MasterToSlaveCallable { + @Override + public FilePath call() throws IOException { + return new FilePath(new File(System.getProperty("user.home"))); + } } /** @@ -2813,6 +3128,11 @@ public final class FilePath implements Serializable { new NamingThreadFactory(new DaemonThreadFactory(), "FilePath.localPool")) )); + + /** + * Channel to the current instance. + */ + @Nonnull public static final LocalChannel localChannel = new LocalChannel(threadPoolForRemoting); private @Nonnull SoloFilePathFilter filterNonNull() { @@ -2900,11 +3220,12 @@ public final class FilePath implements Serializable { return f; } - private boolean mkdirs(File dir) { + private boolean mkdirs(File dir) throws IOException { if (dir.exists()) return false; filterNonNull().mkdirs(dir); - return dir.mkdirs(); + Files.createDirectories(fileToPath(dir)); + return true; } private File mkdirsE(File dir) throws IOException { @@ -2915,5 +3236,115 @@ public final class FilePath implements Serializable { return IOUtils.mkdirs(dir); } + /** + * Check if the relative child is really a descendant after symlink resolution if any. + * + * TODO un-restrict it in a weekly after the patch + */ + @Restricted(NoExternalUse.class) + public boolean isDescendant(@Nonnull String potentialChildRelativePath) throws IOException, InterruptedException { + return act(new IsDescendant(potentialChildRelativePath)); + } + + private class IsDescendant extends SecureFileCallable { + private static final long serialVersionUID = 1L; + private String potentialChildRelativePath; + + private IsDescendant(@Nonnull String potentialChildRelativePath){ + this.potentialChildRelativePath = potentialChildRelativePath; + } + + @Override + public Boolean invoke(@Nonnull File parentFile, @Nonnull VirtualChannel channel) throws IOException, InterruptedException { + if (new File(potentialChildRelativePath).isAbsolute()) { + throw new IllegalArgumentException("Only a relative path is supported, the given path is absolute: " + potentialChildRelativePath); + } + + Path parentAbsolutePath = Util.fileToPath(parentFile.getAbsoluteFile()); + Path parentRealPath; + try { + if (Functions.isWindows()) { + parentRealPath = this.windowsToRealPath(parentAbsolutePath); + } else { + parentRealPath = parentAbsolutePath.toRealPath(); + } + } + catch (NoSuchFileException e) { + LOGGER.log(Level.FINE, String.format("Cannot find the real path to the parentFile: %s", parentAbsolutePath), e); + return false; + } + + // example: "a/b/c" that will become "b/c" then just "c", and finally an empty string + String remainingPath = potentialChildRelativePath; + + Path currentFilePath = parentFile.toPath(); + while (!remainingPath.isEmpty()) { + Path directChild = this.getDirectChild(currentFilePath, remainingPath); + Path childUsingFullPath = currentFilePath.resolve(remainingPath); + String childUsingFullPathAbs = childUsingFullPath.toAbsolutePath().toString(); + String directChildAbs = directChild.toAbsolutePath().toString(); + + if (childUsingFullPathAbs.length() == directChildAbs.length()) { + remainingPath = ""; + } else { + // +1 to avoid the last slash + remainingPath = childUsingFullPathAbs.substring(directChildAbs.length() + 1); + } + + File childFileSymbolic = Util.resolveSymlinkToFile(directChild.toFile()); + if (childFileSymbolic == null) { + currentFilePath = directChild; + } else { + currentFilePath = childFileSymbolic.toPath(); + } + + Path currentFileAbsolutePath = currentFilePath.toAbsolutePath(); + try{ + Path child = currentFileAbsolutePath.toRealPath(); + if (!child.startsWith(parentRealPath)) { + LOGGER.log(Level.FINE, "Child [{0}] does not start with parent [{1}] => not descendant", new Object[]{ child, parentRealPath }); + return false; + } + } catch (NoSuchFileException e) { + // nonexistent file / Windows Server 2016 + MSFT docker + // in case this folder / file will be copied somewhere else, + // it becomes the responsibility of that system to check the isDescendant with the existing links + // we are not taking the parentRealPath to avoid possible problem + Path child = currentFileAbsolutePath.normalize(); + Path parent = parentAbsolutePath.normalize(); + return child.startsWith(parent); + } catch(FileSystemException e) { + LOGGER.log(Level.WARNING, String.format("Problem during call to the method toRealPath on %s", currentFileAbsolutePath), e); + return false; + } + } + + return true; + } + + private @CheckForNull Path getDirectChild(Path parentPath, String childPath){ + Path current = parentPath.resolve(childPath); + while (current != null && !parentPath.equals(current.getParent())) { + current = current.getParent(); + } + return current; + } + + private @Nonnull Path windowsToRealPath(@Nonnull Path path) throws IOException { + try { + return path.toRealPath(); + } + catch (IOException e) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, String.format("relaxedToRealPath cannot use the regular toRealPath on %s, trying with toRealPath(LinkOption.NOFOLLOW_LINKS)", path), e); + } + } + + // that's required for specific environment like Windows Server 2016, running MSFT docker + // where the root is a + return path.toRealPath(LinkOption.NOFOLLOW_LINKS); + } + } + private static final SoloFilePathFilter UNRESTRICTED = SoloFilePathFilter.wrap(FilePathFilter.UNRESTRICTED); } diff --git a/core/src/main/java/hudson/FileSystemProvisioner.java b/core/src/main/java/hudson/FileSystemProvisioner.java index 42d2b6ca53cd71b2493e53e72a8a5896d1d58717..901ff79a6aaa61e36cea879ce645250b27a1365a 100644 --- a/core/src/main/java/hudson/FileSystemProvisioner.java +++ b/core/src/main/java/hudson/FileSystemProvisioner.java @@ -32,6 +32,7 @@ import hudson.model.Job; import hudson.model.TaskListener; import hudson.util.io.ArchiverFactory; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import jenkins.model.Jenkins; import hudson.model.listeners.RunListener; import hudson.scm.SCM; @@ -39,7 +40,6 @@ import org.jenkinsci.Symbol; import java.io.BufferedOutputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -53,7 +53,7 @@ import java.io.OutputStream; * STILL A WORK IN PROGRESS. SUBJECT TO CHANGE! DO NOT EXTEND. * * TODO: is this per {@link Computer}? Per {@link Job}? - * -> probably per agent. + * → probably per agent. * *

Design Problems

*
    @@ -218,6 +218,8 @@ public abstract class FileSystemProvisioner implements ExtensionPoint, Describab File wss = new File(build.getRootDir(),"workspace.tgz"); try (OutputStream os = new BufferedOutputStream(Files.newOutputStream(wss.toPath()))) { ws.archive(ArchiverFactory.TARGZ, os, glob); + } catch (InvalidPathException e) { + throw new IOException(e); } return new WorkspaceSnapshotImpl(); } diff --git a/core/src/main/java/hudson/FileSystemProvisionerDescriptor.java b/core/src/main/java/hudson/FileSystemProvisionerDescriptor.java index 1e02e11f7f4dfdbedf2f00b853535725c1d43f86..9e162d666feb466d4487911fb4b2b2cdbd81a99a 100644 --- a/core/src/main/java/hudson/FileSystemProvisionerDescriptor.java +++ b/core/src/main/java/hudson/FileSystemProvisionerDescriptor.java @@ -50,12 +50,12 @@ public abstract class FileSystemProvisionerDescriptor extends Descriptortrue. + * perform the necessary deletion operation, and return {@code true}. * *

    * If the workspace isn't the one created by this {@link FileSystemProvisioner}, or if the * workspace can be simply deleted by {@link FilePath#deleteRecursive()}, then simply - * return false to give other {@link FileSystemProvisionerDescriptor}s a chance to + * return {@code false} to give other {@link FileSystemProvisionerDescriptor}s a chance to * discard them. * * @param ws diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index 9ff620a8940b31e813cc23385298ebad562f02df..76fdf2f8130a09423ccfc563529d0da528a5ea1e 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -26,6 +26,7 @@ package hudson; import hudson.model.Slave; +import hudson.security.*; import jenkins.util.SystemProperties; import hudson.cli.CLICommand; import hudson.console.ConsoleAnnotationDescriptor; @@ -46,6 +47,7 @@ import hudson.model.JobPropertyDescriptor; import hudson.model.ModelObject; import hudson.model.Node; import hudson.model.PageDecorator; +import jenkins.model.SimplePageDecorator; import hudson.model.PaneStatusProperties; import hudson.model.ParameterDefinition; import hudson.model.ParameterDefinition.ParameterDescriptor; @@ -56,11 +58,6 @@ import hudson.model.View; import hudson.scm.SCM; import hudson.scm.SCMDescriptor; import hudson.search.SearchableModelObject; -import hudson.security.AccessControlled; -import hudson.security.AuthorizationStrategy; -import hudson.security.GlobalSecurityConfiguration; -import hudson.security.Permission; -import hudson.security.SecurityRealm; import hudson.security.captcha.CaptchaSupport; import hudson.security.csrf.CrumbIssuer; import hudson.slaves.Cloud; @@ -125,6 +122,7 @@ import java.util.logging.Logger; import java.util.logging.SimpleFormatter; import java.util.regex.Pattern; +import javax.annotation.Nullable; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; @@ -136,7 +134,6 @@ import jenkins.model.Jenkins; import jenkins.model.ModelObjectWithChildren; import jenkins.model.ModelObjectWithContextMenu; -import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; import org.apache.commons.jelly.JellyContext; import org.apache.commons.jelly.JellyTagException; import org.apache.commons.jelly.Script; @@ -399,11 +396,11 @@ public class Functions { * is chosen, this part remains intact. * *

    - * The 524 is the path from {@link Job} to {@link Run}. + * The {@code 524} is the path from {@link Job} to {@link Run}. * *

    - * The bbb portion is the path after that till the last - * {@link Run} subtype. The ccc portion is the part + * The {@code bbb} portion is the path after that till the last + * {@link Run} subtype. The {@code ccc} portion is the part * after that. */ public static final class RunUrl { @@ -468,11 +465,21 @@ public class Functions { } public static Map getSystemProperties() { - return new TreeMap(System.getProperties()); + return new TreeMap<>(System.getProperties()); + } + + /** + * Gets the system property indicated by the specified key. + * + * Delegates to {@link SystemProperties#getString(java.lang.String)}. + */ + @Restricted(DoNotUse.class) + public static String getSystemProperty(String key) { + return SystemProperties.getString(key); } public static Map getEnvVars() { - return new TreeMap(EnvVars.masterEnvVars); + return new TreeMap<>(EnvVars.masterEnvVars); } public static boolean isWindows() { @@ -537,7 +544,7 @@ public class Functions { * @since 1.525 */ public static Iterable reverse(Collection collection) { - List list = new ArrayList(collection); + List list = new ArrayList<>(collection); Collections.reverse(list); return list; } @@ -611,7 +618,7 @@ public class Functions { private static final SimpleFormatter formatter = new SimpleFormatter(); /** - * Used by layout.jelly to control the auto refresh behavior. + * Used by {@code layout.jelly} to control the auto refresh behavior. * * @param noAutoRefresh * On certain pages, like a page with forms, will have annoying interference @@ -765,7 +772,7 @@ public class Functions { } /** - * This version is so that the 'checkPermission' on layout.jelly + * This version is so that the 'checkPermission' on {@code layout.jelly} * degrades gracefully if "it" is not an {@link AccessControlled} object. * Otherwise it will perform no check and that problem is hard to notice. */ @@ -1103,7 +1110,7 @@ public class Functions { ItemGroup ig = i.getParent(); url = i.getShortUrl()+url; - if(ig== Jenkins.getInstance() || (view != null && ig == view.getOwnerItemGroup())) { + if(ig== Jenkins.getInstance() || (view != null && ig == view.getOwner().getItemGroup())) { assert i instanceof TopLevelItem; if (view != null) { // assume p and the current page belong to the same view, so return a relative path @@ -1135,20 +1142,24 @@ public class Functions { * @since 1.512 */ public static List getAllTopLevelItems(ItemGroup root) { - return Items.getAllItems(root, TopLevelItem.class); + return root.getAllItems(TopLevelItem.class); } /** * Gets the relative name or display name to the given item from the specified group. * * @since 1.515 - * @param p the Item we want the relative display name - * @param g the ItemGroup used as point of reference for the item + * @param p the Item we want the relative display name. + * If {@code null}, a {@code null} will be returned by the method + * @param g the ItemGroup used as point of reference for the item. + * If the group is not specified, item's path will be used. * @param useDisplayName if true, returns a display name, otherwise returns a name * @return - * String like "foo » bar" + * String like "foo » bar". + * {@code null} if item is null or if one of its parents is not an {@link Item}. */ - public static String getRelativeNameFrom(Item p, ItemGroup g, boolean useDisplayName) { + @Nullable + public static String getRelativeNameFrom(@CheckForNull Item p, @CheckForNull ItemGroup g, boolean useDisplayName) { if (p == null) return null; if (g == null) return useDisplayName ? p.getFullDisplayName() : p.getFullName(); String separationString = useDisplayName ? " » " : "/"; @@ -1182,7 +1193,7 @@ public class Functions { if (gr instanceof Item) i = (Item)gr; - else + else // Parent is a group, but not an item return null; } } @@ -1192,11 +1203,14 @@ public class Functions { * * @since 1.515 * @param p the Item we want the relative display name + * If {@code null}, the method will immediately return {@code null}. * @param g the ItemGroup used as point of reference for the item * @return - * String like "foo/bar" + * String like "foo/bar". + * {@code null} if the item is {@code null} or if one of its parents is not an {@link Item}. */ - public static String getRelativeNameFrom(Item p, ItemGroup g) { + @Nullable + public static String getRelativeNameFrom(@CheckForNull Item p, @CheckForNull ItemGroup g) { return getRelativeNameFrom(p, g, false); } @@ -1205,12 +1219,15 @@ public class Functions { * Gets the relative display name to the given item from the specified group. * * @since 1.512 - * @param p the Item we want the relative display name + * @param p the Item we want the relative display name. + * If {@code null}, the method will immediately return {@code null}. * @param g the ItemGroup used as point of reference for the item * @return - * String like "Foo » Bar" + * String like "Foo » Bar". + * {@code null} if the item is {@code null} or if one of its parents is not an {@link Item}. */ - public static String getRelativeDisplayNameFrom(Item p, ItemGroup g) { + @Nullable + public static String getRelativeDisplayNameFrom(@CheckForNull Item p, @CheckForNull ItemGroup g) { return getRelativeNameFrom(p, g, true); } @@ -1361,6 +1378,7 @@ public class Functions { } public static String jsStringEscape(String s) { + if (s == null) return null; StringBuilder buf = new StringBuilder(); for( int i=0; i - * This is primarily used in slave-agent.jnlp.jelly to specify the destination + * This is primarily used in {@code slave-agent.jnlp.jelly} to specify the destination * that the agents talk to. */ public String getServerName() { @@ -1728,7 +1746,7 @@ public class Functions { /** * If the given href link is matching the current page, return true. * - * Used in task.jelly to decide if the page should be highlighted. + * Used in {@code task.jelly} to decide if the page should be highlighted. */ public boolean hyperlinkMatchesCurrentPage(String href) throws UnsupportedEncodingException { String url = Stapler.getCurrentRequest().getRequestURL().toString(); @@ -1753,7 +1771,18 @@ public class Functions { if(Jenkins.getInstanceOrNull()==null) return Collections.emptyList(); return PageDecorator.all(); } - + /** + * Gets only one {@link SimplePageDecorator}. + * @since 2.128 + */ + public static SimplePageDecorator getSimplePageDecorator() { + return SimplePageDecorator.first(); + } + + public static List getSimplePageDecorators() { + return SimplePageDecorator.all(); + } + public static List> getCloudDescriptors() { return Cloud.all(); } @@ -1798,11 +1827,11 @@ public class Functions { } /** - * Generate a series of <script> tags to include script.js + * Generate a series of {@code -