├── .editorconfig ├── .github ├── release.yml └── workflows │ ├── build.yaml │ └── release.yaml ├── .gitignore ├── Dockerfile ├── ISSUE_TEMPLATE.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── build.gradle ├── cli ├── build.gradle ├── gssh-example.groovy └── src │ ├── main │ └── groovy │ │ └── org │ │ └── hidetake │ │ └── groovy │ │ └── ssh │ │ ├── Main.groovy │ │ └── Runtime.groovy │ └── test │ ├── groovy │ └── org │ │ └── hidetake │ │ └── groovy │ │ └── ssh │ │ ├── MainDryRunSpec.groovy │ │ └── MainSpec.groovy │ └── resources │ ├── hostkey_dsa │ └── hostkey_dsa.pub ├── core ├── bin │ └── .gitignore ├── build.gradle └── src │ ├── main │ ├── groovy │ │ └── org │ │ │ └── hidetake │ │ │ └── groovy │ │ │ └── ssh │ │ │ ├── Release.groovy │ │ │ ├── Ssh.groovy │ │ │ ├── connection │ │ │ ├── AddHostKey.groovy │ │ │ ├── AllowAnyHosts.groovy │ │ │ ├── Connection.groovy │ │ │ ├── ConnectionManager.groovy │ │ │ ├── ConnectionSettings.groovy │ │ │ ├── HostAuthentication.groovy │ │ │ ├── HostAuthenticationSettings.groovy │ │ │ ├── HostKeyRepository.groovy │ │ │ ├── JSchLogger.groovy │ │ │ ├── ProxyConnection.groovy │ │ │ ├── ProxyConnectionSettings.groovy │ │ │ ├── ProxyValidator.groovy │ │ │ ├── UserAuthentication.groovy │ │ │ └── UserAuthenticationSettings.groovy │ │ │ ├── core │ │ │ ├── ParallelSessionsException.groovy │ │ │ ├── Proxy.groovy │ │ │ ├── ProxyType.java │ │ │ ├── Remote.groovy │ │ │ ├── RunHandler.groovy │ │ │ ├── Service.groovy │ │ │ ├── container │ │ │ │ ├── Container.groovy │ │ │ │ ├── ContainerBuilder.groovy │ │ │ │ ├── ProxyContainer.groovy │ │ │ │ ├── RemoteContainer.groovy │ │ │ │ └── RoleAccessible.groovy │ │ │ ├── settings │ │ │ │ ├── CompositeSettings.groovy │ │ │ │ ├── GlobalSettings.groovy │ │ │ │ ├── LoggingMethod.java │ │ │ │ ├── PerServiceSettings.groovy │ │ │ │ ├── SettingsHelper.groovy │ │ │ │ └── ToStringProperties.groovy │ │ │ └── type │ │ │ │ └── InputStreamValue.groovy │ │ │ ├── interaction │ │ │ ├── Buffer.groovy │ │ │ ├── BufferRule.groovy │ │ │ ├── Context.groovy │ │ │ ├── InteractionException.groovy │ │ │ ├── InteractionHandler.groovy │ │ │ ├── Interactions.groovy │ │ │ ├── Listener.groovy │ │ │ ├── MatchResult.groovy │ │ │ ├── Processor.groovy │ │ │ ├── Receiver.groovy │ │ │ ├── Rule.groovy │ │ │ ├── Stream.java │ │ │ ├── StreamRule.groovy │ │ │ └── Wildcard.groovy │ │ │ ├── operation │ │ │ ├── Command.groovy │ │ │ ├── CommandSettings.groovy │ │ │ ├── DefaultOperations.groovy │ │ │ ├── DryRunOperation.groovy │ │ │ ├── DryRunOperations.groovy │ │ │ ├── Operation.groovy │ │ │ ├── Operations.groovy │ │ │ ├── SftpError.java │ │ │ ├── SftpException.groovy │ │ │ ├── SftpOperations.groovy │ │ │ ├── SftpProgress.groovy │ │ │ ├── Shell.groovy │ │ │ └── ShellSettings.groovy │ │ │ ├── session │ │ │ ├── BadExitStatusException.groovy │ │ │ ├── Session.groovy │ │ │ ├── SessionExtension.groovy │ │ │ ├── SessionExtensions.groovy │ │ │ ├── SessionHandler.groovy │ │ │ ├── SessionSettings.groovy │ │ │ ├── SessionTask.groovy │ │ │ ├── execution │ │ │ │ ├── BackgroundCommand.groovy │ │ │ │ ├── Command.groovy │ │ │ │ ├── Escape.groovy │ │ │ │ ├── Script.groovy │ │ │ │ ├── Shell.groovy │ │ │ │ ├── Sudo.groovy │ │ │ │ ├── SudoException.groovy │ │ │ │ ├── SudoHelper.groovy │ │ │ │ └── SudoSettings.groovy │ │ │ ├── forwarding │ │ │ │ ├── LocalPortForwardSettings.groovy │ │ │ │ ├── PortForward.groovy │ │ │ │ └── RemotePortForwardSettings.groovy │ │ │ └── transfer │ │ │ │ ├── FileGet.groovy │ │ │ │ ├── FilePut.groovy │ │ │ │ ├── FileTransferMethod.java │ │ │ │ ├── FileTransferSettings.groovy │ │ │ │ ├── SftpRemove.groovy │ │ │ │ ├── get │ │ │ │ ├── FileReceiver.groovy │ │ │ │ ├── Provider.groovy │ │ │ │ ├── RecursiveReceiver.groovy │ │ │ │ ├── Scp.groovy │ │ │ │ ├── ScpException.groovy │ │ │ │ ├── Sftp.groovy │ │ │ │ ├── StreamReceiver.groovy │ │ │ │ └── WritableReceiver.groovy │ │ │ │ └── put │ │ │ │ ├── EnterDirectory.groovy │ │ │ │ ├── Instructions.groovy │ │ │ │ ├── LeaveDirectory.groovy │ │ │ │ ├── Provider.groovy │ │ │ │ ├── Scp.groovy │ │ │ │ ├── Sftp.groovy │ │ │ │ └── StreamContent.groovy │ │ │ └── util │ │ │ ├── FileTransferProgress.groovy │ │ │ ├── ManagedBlocking.groovy │ │ │ └── Utility.groovy │ └── resources │ │ └── org │ │ └── hidetake │ │ └── groovy │ │ └── ssh │ │ └── Release.properties │ └── test │ └── groovy │ └── org │ └── hidetake │ └── groovy │ └── ssh │ ├── SshClassSpec.groovy │ ├── connection │ ├── ConnectionSettingsSpec.groovy │ ├── HostKeyRepositorySpec.groovy │ └── ProxyValidatorSpec.groovy │ ├── core │ ├── ProxySpec.groovy │ ├── RemoteSpec.groovy │ ├── RunHandlerSpec.groovy │ ├── ServiceSpec.groovy │ ├── container │ │ ├── ContainerBuilderSpec.groovy │ │ └── RemoteContainerSpec.groovy │ └── settings │ │ ├── SettingsHelperSpec.groovy │ │ └── ToStringPropertiesSpec.groovy │ ├── interaction │ ├── InteractionHandlerSpec.groovy │ └── RuleSpec.groovy │ ├── session │ ├── SessionExtensionSpec.groovy │ ├── SessionHandlerSpec.groovy │ ├── SessionSettingsSpec.groovy │ └── execution │ │ └── EscapeSpec.groovy │ └── util │ ├── FileTransferProgressSpec.groovy │ └── RetrySpec.groovy ├── docs ├── build.gradle └── src │ └── docs │ └── asciidoc │ ├── example-script.adoc │ ├── getting-started.adoc │ ├── index.adoc │ ├── introduction.adoc │ ├── migration-guide.adoc │ ├── user-guide.adoc │ └── version-loader.adoc ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── os-integration-test ├── bin │ └── .gitignore ├── build.gradle ├── etc │ └── ssh │ │ ├── id_ecdsa │ │ ├── id_ecdsa.pub │ │ ├── id_rsa │ │ ├── id_rsa.pub │ │ ├── id_rsa_pass │ │ ├── id_rsa_pass.pub │ │ ├── known_hosts │ │ ├── ssh_host_dsa_key │ │ ├── ssh_host_dsa_key.pub │ │ ├── ssh_host_ecdsa_key │ │ ├── ssh_host_ecdsa_key.pub │ │ ├── ssh_host_ed25519_key │ │ ├── ssh_host_ed25519_key.pub │ │ ├── ssh_host_rsa_key │ │ └── ssh_host_rsa_key.pub ├── run-sshd.sh └── src │ ├── main │ └── groovy │ │ └── org │ │ └── hidetake │ │ └── groovy │ │ └── ssh │ │ └── test │ │ └── os │ │ ├── FileDivCategory.groovy │ │ ├── Fixture.groovy │ │ ├── MkdirType.groovy │ │ ├── RemoteFixture.groovy │ │ ├── SshAgent.groovy │ │ └── UserManagementExtension.groovy │ └── test │ ├── groovy │ └── org │ │ └── hidetake │ │ └── groovy │ │ └── ssh │ │ └── test │ │ └── os │ │ ├── AbstractFileTransferSpec.groovy │ │ ├── CommandSpec.groovy │ │ ├── GatewaySpec.groovy │ │ ├── HostAuthenticationSpec.groovy │ │ ├── ScpSpec.groovy │ │ ├── ScriptSpec.groovy │ │ ├── SftpSpec.groovy │ │ ├── ShellSpec.groovy │ │ ├── SudoSpec.groovy │ │ └── UserAuthenticationSpec.groovy │ └── resources │ └── logback.xml ├── plugin-integration ├── create-branch-for-release.sh └── run-plugin-integration-test.sh ├── server-integration-test ├── bin │ └── .gitignore ├── build.gradle └── src │ ├── main │ └── groovy │ │ └── org │ │ └── hidetake │ │ └── groovy │ │ └── ssh │ │ └── test │ │ └── server │ │ ├── CommandHelper.groovy │ │ ├── FileDivCategory.groovy │ │ ├── FilenameUtils.groovy │ │ ├── HostKeyFixture.groovy │ │ ├── SshServerMock.groovy │ │ ├── SudoHelper.groovy │ │ └── UserKeyFixture.groovy │ └── test │ ├── groovy │ └── org │ │ └── hidetake │ │ └── groovy │ │ └── ssh │ │ └── test │ │ └── server │ │ ├── AbstractFileTransferSpec.groovy │ │ ├── CommandSpec.groovy │ │ ├── DryRunSpec.groovy │ │ ├── ExtensionSpec.groovy │ │ ├── GatewaySpec.groovy │ │ ├── HostAuthenticationSpec.groovy │ │ ├── ParallelSessionsSpec.groovy │ │ ├── PortForwardingSpec.groovy │ │ ├── RetrySpec.groovy │ │ ├── ScpSpec.groovy │ │ ├── ScriptSpec.groovy │ │ ├── SftpRemoveSpec.groovy │ │ ├── SftpSpec.groovy │ │ ├── ShellSpec.groovy │ │ ├── SudoSpec.groovy │ │ ├── TimeoutSpec.groovy │ │ └── UserAuthenticationSpec.groovy │ └── resources │ ├── hostkey_ecdsa-sha2-nistp256 │ ├── hostkey_ecdsa-sha2-nistp256.pub │ ├── hostkey_ecdsa-sha2-nistp256_another.pub │ ├── hostkey_ssh-dss │ ├── hostkey_ssh-dss.pub │ ├── hostkey_ssh-rsa │ ├── hostkey_ssh-rsa.pub │ ├── id_ecdsa │ ├── id_ecdsa.pub │ ├── id_ecdsa_pass │ ├── id_ecdsa_pass.pub │ └── logback.xml └── settings.gradle /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | indent_size = 2 8 | 9 | [*.{groovy,java,gradle}] 10 | indent_size = 4 11 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes 2 | changelog: 3 | categories: 4 | - title: Features 5 | labels: 6 | - '*' 7 | exclude: 8 | labels: 9 | - renovate 10 | - refactoring 11 | - title: Refactoring 12 | labels: 13 | - refactoring 14 | - title: Dependencies 15 | labels: 16 | - renovate 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | gradle: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 30 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-java@v4 16 | with: 17 | distribution: temurin 18 | java-version: 11 19 | - uses: gradle/actions/wrapper-validation@v4 20 | - uses: gradle/actions/setup-gradle@v4 21 | - run: ./gradlew build --warning-mode all 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - core/** 7 | - .github/workflows/release.yaml 8 | - gradle/** 9 | - '*.gradle' 10 | release: 11 | types: 12 | - created 13 | 14 | env: 15 | GROOVY_SSH_VERSION: ${{ github.event.release.tag_name }} 16 | 17 | jobs: 18 | publish: 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 10 21 | permissions: 22 | contents: read 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-java@v4 26 | with: 27 | distribution: temurin 28 | java-version: 11 29 | - uses: gradle/actions/wrapper-validation@v4 30 | - uses: gradle/actions/setup-gradle@v4 31 | 32 | - run: ./gradlew sign 33 | env: 34 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 35 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 36 | 37 | - if: github.event_name == 'release' 38 | run: ./gradlew publish 39 | env: 40 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 41 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 42 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 43 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 44 | 45 | cli: 46 | runs-on: ubuntu-latest 47 | timeout-minutes: 10 48 | permissions: 49 | contents: write 50 | steps: 51 | - uses: actions/checkout@v4 52 | - uses: actions/setup-java@v4 53 | with: 54 | distribution: temurin 55 | java-version: 11 56 | - uses: gradle/actions/wrapper-validation@v4 57 | - uses: gradle/actions/setup-gradle@v4 58 | - run: ./gradlew shadowJar 59 | - run: java -jar cli/build/libs/gssh.jar 60 | - run: sha256sum -b cli/build/libs/gssh.jar > cli/build/libs/gssh.jar.sha256 61 | - if: github.event_name == 'release' 62 | run: gh release upload '${{ github.event.release.tag_name }}' cli/build/libs/gssh.jar cli/build/libs/gssh.jar.sha256 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build 3 | 4 | **/.settings 5 | .classpath 6 | .project 7 | 8 | .idea 9 | *.iml 10 | .vscode/ 11 | 12 | /plugin-integration/gradle-ssh-plugin 13 | 14 | bin/ 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | from java:8 2 | 3 | volume /usr/src/groovy-ssh 4 | copy . /usr/src/groovy-ssh 5 | run cd /usr/src/groovy-ssh && ./gradlew -g .gradle shadowJar && cp -a cli/build/libs/gssh.jar / 6 | 7 | entrypoint ["java", "-jar", "/gssh.jar"] 8 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Environment info 4 | Please paste the output of `println ssh.version` e.g. `groovy-ssh-x.x.x (java-1.8.0_xx, groovy-2.4.x, jsch-0.1.x)`. 5 | 6 | If it is difficult, describe version of Groovy SSH, JVM and OS. 7 | 8 | 9 | ### Steps to reproduce 10 | 1. 11 | 2. 12 | 3. 13 | 14 | ```groovy 15 | // Paste the snippet of script or logs 16 | ``` 17 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | This adds the feature | fixes the bug... 2 | 3 | 4 | ### Steps to use the feature | verify the fix 5 | 1. 6 | 2. 7 | 3. 8 | 9 | ```groovy 10 | // Paste the snippet 11 | ``` 12 | 13 | 14 | ### Backward compatibility 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Groovy SSH [![build](https://github.com/int128/groovy-ssh/actions/workflows/build.yaml/badge.svg)](https://github.com/int128/groovy-ssh/actions/workflows/build.yaml) 2 | ========== 3 | 4 | Groovy SSH is an automation tool based on DSL providing the remote command execution and file transfer. 5 | 6 | ## Moved :warning: 7 | 8 | This repository has been moved to https://github.com/int128/gradle-ssh-plugin. 9 | 10 | ---- 11 | 12 | Contributions 13 | ------------- 14 | 15 | This is an open source software licensed under the Apache License Version 2.0. 16 | Feel free to open issues or pull requests. 17 | 18 | 19 | ### Unit test 20 | 21 | We can run the unit test as follows: 22 | 23 | ```sh 24 | ./gradlew :core:check 25 | ``` 26 | 27 | 28 | ### Server integration test 29 | 30 | We can run the server integration test using Apache MINA SSHD server as follows: 31 | 32 | ```sh 33 | ./gradlew :server-integration-test:check 34 | ``` 35 | 36 | 37 | ### CLI test 38 | 39 | We can run the integration test of CLI as follows: 40 | 41 | ```sh 42 | ./gradlew :cli:check 43 | ``` 44 | 45 | 46 | ### OS integration test 47 | 48 | We can run the OS integration tests using [int128/sshd](https://github.com/int128/docker-sshd) image as follows: 49 | 50 | ```sh 51 | # Run a sshd container 52 | ./os-integration-test/run-sshd.sh 53 | 54 | # Run the tests 55 | ./gradlew :os-integration-test:check 56 | ``` 57 | 58 | 59 | ### Gradle SSH Plugin integration test 60 | 61 | We can run the test with Gradle SSH Plugin. 62 | See `plugin-integration/run-plugin-integration-test.sh` for details. 63 | 64 | If you are planning to release with specification change breaking backward compatibility, 65 | create `groovy-ssh-acceptance-test` branch on Gradle SSH Plugin to pass the acceptance test. 66 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | group = 'org.hidetake' 3 | version = System.getenv('GROOVY_SSH_VERSION') ?: '0.0.0' 4 | } 5 | -------------------------------------------------------------------------------- /cli/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'groovy' 3 | id 'com.github.johnrengelman.shadow' version '7.1.2' 4 | } 5 | 6 | repositories { 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | implementation project(':core') 12 | implementation 'ch.qos.logback:logback-classic:1.5.18' 13 | implementation 'org.codehaus.groovy:groovy-cli-commons:3.0.24' 14 | 15 | testImplementation project(':server-integration-test') 16 | testImplementation 'org.apache.sshd:sshd-core:2.2.0' 17 | testImplementation platform("org.spockframework:spock-bom:2.3-groovy-3.0") 18 | testImplementation "org.spockframework:spock-core" 19 | testImplementation "org.spockframework:spock-junit4" 20 | testRuntimeOnly 'ch.qos.logback:logback-classic:1.5.18' 21 | } 22 | 23 | test { 24 | mustRunAfter ':server-integration-test:check' 25 | useJUnitPlatform() 26 | } 27 | 28 | jar { 29 | manifest { 30 | attributes 'Main-Class': 'org.hidetake.groovy.ssh.Main' 31 | } 32 | } 33 | 34 | shadowJar { 35 | archiveBaseName = 'gssh' 36 | archiveVersion = '' 37 | archiveClassifier = '' 38 | } 39 | -------------------------------------------------------------------------------- /cli/gssh-example.groovy: -------------------------------------------------------------------------------- 1 | ssh.remotes { 2 | tester { 3 | host = 'localhost' 4 | port = 22 5 | user = 'tester' 6 | identity = new File('os-integration-test/etc/ssh/id_rsa') 7 | knownHosts = addHostKey(new File('cli/build/known_hosts')) 8 | } 9 | } 10 | 11 | ssh.run { 12 | session(ssh.remotes.tester) { 13 | execute 'uname -a' 14 | } 15 | } 16 | 17 | assert new File('cli/build/known_hosts').readLines().any { line -> 18 | line.startsWith('localhost ssh-rsa') 19 | } 20 | -------------------------------------------------------------------------------- /cli/src/main/groovy/org/hidetake/groovy/ssh/Main.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh 2 | 3 | import ch.qos.logback.classic.Level 4 | import groovy.cli.commons.CliBuilder 5 | import groovy.util.logging.Slf4j 6 | import org.hidetake.groovy.ssh.core.Service 7 | 8 | /** 9 | * CLI main class. 10 | * 11 | * @author Hidetake Iwata 12 | */ 13 | @Slf4j 14 | class Main { 15 | static void main(String[] args) { 16 | def cli = new CliBuilder( 17 | usage: '[option...] [-e script-text] [script-filename | --stdin] [script-args...]', 18 | width: 120 19 | ) 20 | cli.h longOpt: 'help', 'Shows this help message.' 21 | cli.q longOpt: 'quiet', 'Set log level to warn.' 22 | cli.i longOpt: 'info', 'Set log level to info. (default)' 23 | cli.d longOpt: 'debug', 'Set log level to debug.' 24 | cli._ longOpt: 'stdin', 'Specify standard input as a source.' 25 | cli.e args: 1, 'Specify a command line script.' 26 | cli.n longOpt: 'dry-run', 'Do a dry run without connections.' 27 | cli._ longOpt: 'version', 'Shows version.' 28 | cli.s longOpt: 'stacktrace', 'Print out the stacktrace for the exception.' 29 | 30 | def options = cli.parse(args) 31 | if (!options || options.h) { 32 | cli.usage() 33 | } else if (options.version) { 34 | println Ssh.release 35 | } else { 36 | Runtime.instance.logback(level: logLevel(options)) 37 | 38 | if (!options.s) { 39 | Thread.currentThread().uncaughtExceptionHandler = { Thread t, Throwable e -> 40 | log.error("Error: $e") 41 | log.info('Run with -s or --stacktrace option to get the stack trace') 42 | } 43 | } 44 | 45 | def shell = newShellWith(options) 46 | def extra = options.arguments() 47 | 48 | if (options.e) { shell.run(options.e as String, 'script.groovy', extra) } 49 | else if (options.stdin) { shell.run(System.in.newReader(), 'script.groovy', extra) } 50 | else if (!extra.empty) { shell.run(new File(extra.head()), extra.tail()) } 51 | else { cli.usage() } 52 | } 53 | } 54 | 55 | private static logLevel(options) { 56 | if (options.d) { Level.DEBUG } 57 | else if (options.i) { Level.INFO } 58 | else if (options.q) { Level.WARN } 59 | else { Level.INFO } 60 | } 61 | 62 | private static newShellWith(options) { 63 | def shell = Ssh.newShell() 64 | def service = shell.getVariable('ssh') as Service 65 | service.metaClass.runtime = Runtime.instance 66 | service.metaClass.version = Ssh.release.toString() 67 | if (options.n) { 68 | service.settings { 69 | dryRun = true 70 | } 71 | } 72 | shell 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /cli/src/main/groovy/org/hidetake/groovy/ssh/Runtime.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh 2 | 3 | import ch.qos.logback.classic.Level 4 | import ch.qos.logback.classic.encoder.PatternLayoutEncoder 5 | import ch.qos.logback.core.ConsoleAppender 6 | import org.slf4j.Logger 7 | import org.slf4j.LoggerFactory 8 | 9 | /** 10 | * Runtime info. 11 | * 12 | * @author Hidetake Iwata 13 | */ 14 | @Singleton 15 | class Runtime { 16 | 17 | /** 18 | * Configure logback. 19 | * @param settings 20 | * level: log level ({@link String} or {@link Level}), 21 | * pattern: format ({@link String}) 22 | */ 23 | void logback(Map settings) { 24 | def root = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) 25 | assert root instanceof ch.qos.logback.classic.Logger 26 | def originalLevel = root.level 27 | 28 | def encoder = new PatternLayoutEncoder() 29 | encoder.context = root.loggerContext 30 | encoder.pattern = settings.pattern ?: '%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %msg%n' 31 | encoder.start() 32 | 33 | def appender = new ConsoleAppender() 34 | appender.context = root.loggerContext 35 | appender.encoder = encoder 36 | appender.name = 'console' 37 | appender.start() 38 | 39 | root.loggerContext.reset() 40 | root.addAppender(appender) 41 | 42 | switch (settings.level) { 43 | case String: 44 | root.level = Level.toLevel(settings.level as String) 45 | break 46 | case Level: 47 | root.level = settings.level 48 | break 49 | default: 50 | root.level = originalLevel 51 | break 52 | } 53 | } 54 | 55 | /** 56 | * Path to self Groovy SSH JAR, or null if it is unknown 57 | */ 58 | @Lazy 59 | File jar = { 60 | def url = Runtime.getResource("/${Runtime.name.replace('.', '/')}.class") 61 | if (url.protocol == 'jar') { 62 | def m = url.file =~ /^file:(.+?)!/ 63 | if (m) { 64 | def jarFile = new File(m.group(1)) 65 | assert jarFile.exists() 66 | jarFile 67 | } else { 68 | null 69 | } 70 | } else { 71 | null 72 | } 73 | }() 74 | 75 | } 76 | -------------------------------------------------------------------------------- /cli/src/test/groovy/org/hidetake/groovy/ssh/MainDryRunSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh 2 | 3 | import com.jcraft.jsch.JSchException 4 | import org.hidetake.groovy.ssh.test.server.SshServerMock 5 | import org.junit.ClassRule 6 | import org.junit.rules.TemporaryFolder 7 | import spock.lang.Shared 8 | import spock.lang.Specification 9 | import spock.lang.Unroll 10 | 11 | import static org.hidetake.groovy.ssh.test.server.FilenameUtils.toUnixPath 12 | 13 | class MainDryRunSpec extends Specification { 14 | 15 | @Shared 16 | String script 17 | 18 | ByteArrayOutputStream stdoutBuffer 19 | 20 | PrintStream stdout 21 | 22 | @Shared @ClassRule 23 | TemporaryFolder temporaryFolder 24 | 25 | def setupSpec() { 26 | int port = SshServerMock.pickUpFreePort() 27 | def knownHostsFile = temporaryFolder.newFile() << "[localhost]:${port} dummy" 28 | script = """\ 29 | ssh.run { 30 | session( 31 | host: 'localhost', 32 | port: ${port}, 33 | knownHosts: new File('${toUnixPath(knownHostsFile.path)}'), 34 | user: 'someuser', 35 | password: 'somepassword' 36 | ) { 37 | execute('somecommand') { println 'Q6zLyqR1MKANtYJ4' } 38 | } 39 | } 40 | """ 41 | } 42 | 43 | def setup() { 44 | stdout = System.out 45 | stdoutBuffer = new ByteArrayOutputStream() 46 | System.out = new PrintStream(stdoutBuffer) 47 | } 48 | 49 | def cleanup() { 50 | System.out = stdout 51 | } 52 | 53 | def "script should fail due to connection refused"() { 54 | when: 55 | Main.main '-e', script 56 | 57 | then: 58 | JSchException e = thrown() 59 | e.cause instanceof ConnectException 60 | e.cause.message.startsWith 'Connection refused' 61 | } 62 | 63 | @Unroll 64 | def "flag #flag should enable dry run"() { 65 | when: 66 | Main.main flag, '-e', script 67 | 68 | then: 69 | stdoutBuffer.toString('UTF-8').contains('Q6zLyqR1MKANtYJ4') 70 | 71 | where: 72 | flag << ['--dry-run', '-n'] 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /cli/src/test/resources/hostkey_dsa: -------------------------------------------------------------------------------- 1 | -----BEGIN DSA PRIVATE KEY----- 2 | MIIBugIBAAKBgQDr7tBpE/LrkghfUbe4issLb3tcLTZlpCDxC2Vy1f8E4vkpua8R 3 | eSBSJxuXSzaLnObhGc4s5y38/mR+BS+OLCW2vH57OTXShbINlGF8Y5okjL7tc0eo 4 | owHzrDyJ1U0UeBBCkmmCPJjZna4xNGnakKXkBRRLheHMCpd+X/3HExwO+wIVAIRI 5 | nG3eitk3sM4zhEihAvlsX0R/AoGAJC3mvwWmOmgM+djKPHpdSPx+gRFFV6nIGeay 6 | dJnJZfDpZ9UEVhFGBVIIj7TVHMA8WeXLdEaIZvYfy1sECWTWhJE0aXYzHVYx/N5C 7 | GixHH0oYSPGtmbfRbTJ/itoHHbBlZAMN2mhdiRD/hgmUndmUiNjOfjAzGfSPoTSo 8 | 1eFI3QICgYBRt25Cp4EQ0zX48BLKhRylZ7IgcBi7CvzHNUgvRfQJ78b/7CG10pYn 9 | PjRs4OqM0YkccklGrOXafFwTa+iK8PsBU/4IAy4XnJ5XStE7FrGEfVhlyOr+bi6C 10 | pjXk3opqJjt+5ImXSE6+bZ8JMt86B626Na+wba7QqU6NR/6viRxXRwIUUKFC8L/H 11 | id4VH959BWtteLkCxng= 12 | -----END DSA PRIVATE KEY----- -------------------------------------------------------------------------------- /cli/src/test/resources/hostkey_dsa.pub: -------------------------------------------------------------------------------- 1 | ssh-dss AAAAB3NzaC1kc3MAAACBAOvu0GkT8uuSCF9Rt7iKywtve1wtNmWkIPELZXLV/wTi+Sm5rxF5IFInG5dLNouc5uEZziznLfz+ZH4FL44sJba8fns5NdKFsg2UYXxjmiSMvu1zR6ijAfOsPInVTRR4EEKSaYI8mNmdrjE0adqQpeQFFEuF4cwKl35f/ccTHA77AAAAFQCESJxt3orZN7DOM4RIoQL5bF9EfwAAAIAkLea/BaY6aAz52Mo8el1I/H6BEUVXqcgZ5rJ0mcll8Oln1QRWEUYFUgiPtNUcwDxZ5ct0Rohm9h/LWwQJZNaEkTRpdjMdVjH83kIaLEcfShhI8a2Zt9FtMn+K2gcdsGVkAw3aaF2JEP+GCZSd2ZSI2M5+MDMZ9I+hNKjV4UjdAgAAAIBRt25Cp4EQ0zX48BLKhRylZ7IgcBi7CvzHNUgvRfQJ78b/7CG10pYnPjRs4OqM0YkccklGrOXafFwTa+iK8PsBU/4IAy4XnJ5XStE7FrGEfVhlyOr+bi6CpjXk3opqJjt+5ImXSE6+bZ8JMt86B626Na+wba7QqU6NR/6viRxXRw== 2 | -------------------------------------------------------------------------------- /core/bin/.gitignore: -------------------------------------------------------------------------------- 1 | /main/ 2 | /test/ 3 | -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'groovy' 4 | id 'maven-publish' 5 | id 'signing' 6 | } 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | api localGroovy() 14 | api 'com.github.mwiede:jsch:0.2.26' 15 | api 'org.slf4j:slf4j-api:2.0.17' 16 | 17 | testImplementation platform("org.spockframework:spock-bom:2.3-groovy-3.0") 18 | testImplementation "org.spockframework:spock-core" 19 | testImplementation "org.spockframework:spock-junit4" 20 | testRuntimeOnly 'cglib:cglib-nodep:3.3.0' 21 | testRuntimeOnly 'org.objenesis:objenesis:3.4' 22 | testRuntimeOnly 'ch.qos.logback:logback-classic:1.5.18' 23 | } 24 | 25 | test { 26 | useJUnitPlatform() 27 | } 28 | 29 | processResources { 30 | filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: ['version': project.version]) 31 | } 32 | 33 | java { 34 | withJavadocJar() 35 | withSourcesJar() 36 | } 37 | 38 | description = 'Groovy SSH library' 39 | 40 | publishing { 41 | repositories { 42 | maven { 43 | name = "OSSRH" 44 | url = "https://oss.sonatype.org/service/local/staging/deploy/maven2" 45 | credentials { 46 | username = System.getenv("OSSRH_USERNAME") 47 | password = System.getenv("OSSRH_PASSWORD") 48 | } 49 | } 50 | } 51 | 52 | publications { 53 | maven(MavenPublication) { 54 | // https://central.sonatype.org/publish/requirements/ 55 | artifactId parent.name 56 | from components.java 57 | pom { 58 | name = parent.name 59 | description = project.description 60 | url = 'https://github.com/int128/groovy-ssh' 61 | licenses { 62 | license { 63 | name = 'Apache-2.0' 64 | url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' 65 | } 66 | } 67 | developers { 68 | developer { 69 | id = 'int128' 70 | name = 'Hidetake Iwata' 71 | email = 'int128@gmail.com' 72 | } 73 | } 74 | scm { 75 | url = 'https://github.com/int128/groovy-ssh' 76 | connection = 'scm:git:https://github.com/int128/groovy-ssh' 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | signing { 84 | useInMemoryPgpKeys(System.getenv("SIGNING_KEY"), System.getenv("SIGNING_PASSWORD")) 85 | sign publishing.publications 86 | } 87 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/Release.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh 2 | 3 | import com.jcraft.jsch.JSch 4 | 5 | /** 6 | * Release metadata. 7 | * 8 | * @author Hidetake Iwata 9 | */ 10 | class Release { 11 | 12 | private final bundle = ResourceBundle.getBundle(Release.class.name) 13 | 14 | final String name = bundle.getString('product.name') 15 | final String version = bundle.getString('product.version') 16 | 17 | final String javaVersion = System.getProperty('java.version') 18 | final String groovyVersion = GroovySystem.version 19 | final String jschVersion = JSch.VERSION 20 | 21 | @Override 22 | String toString() { 23 | "$name-$version (java-$javaVersion, groovy-$groovyVersion, jsch-$jschVersion)" 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/Ssh.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh 2 | 3 | import groovy.transform.CompileStatic 4 | import org.hidetake.groovy.ssh.core.Service 5 | 6 | /** 7 | * Entry point of Groovy SSH library. 8 | * 9 | * @author Hidetake Iwata 10 | */ 11 | @CompileStatic 12 | class Ssh { 13 | /** 14 | * Create an instance of {@link Service}. 15 | */ 16 | static Service newService() { 17 | new Service() 18 | } 19 | 20 | /** 21 | * Create a {@link GroovyShell} object to run a Groovy script. 22 | */ 23 | static GroovyShell newShell() { 24 | def binding = new Binding() 25 | binding.variables.ssh = newService() 26 | new GroovyShell(binding) 27 | } 28 | 29 | /** 30 | * Return the release metadata. 31 | */ 32 | @Lazy 33 | static Release release = { new Release() }() 34 | 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/connection/AddHostKey.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.connection 2 | 3 | class AddHostKey { 4 | 5 | final File knownHostsFile 6 | 7 | def AddHostKey(File knownHostsFile1) { 8 | knownHostsFile = knownHostsFile1 9 | assert knownHostsFile 10 | } 11 | 12 | @Override 13 | String toString() { 14 | "addHostKey($knownHostsFile)" 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/connection/AllowAnyHosts.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.connection 2 | 3 | @Singleton 4 | class AllowAnyHosts { 5 | @Override 6 | String toString() { 7 | 'allowAnyHosts' 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/connection/Connection.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.connection 2 | 3 | import com.jcraft.jsch.ChannelExec 4 | import com.jcraft.jsch.ChannelSftp 5 | import com.jcraft.jsch.ChannelShell 6 | import com.jcraft.jsch.Session 7 | import groovy.util.logging.Slf4j 8 | import org.hidetake.groovy.ssh.core.Remote 9 | import org.hidetake.groovy.ssh.session.forwarding.LocalPortForwardSettings 10 | import org.hidetake.groovy.ssh.session.forwarding.RemotePortForwardSettings 11 | 12 | /** 13 | * A connected SSH connection. 14 | * 15 | * @author Hidetake Iwata 16 | */ 17 | @Slf4j 18 | class Connection { 19 | final Remote remote 20 | final Session session 21 | 22 | /** 23 | * Constructor 24 | * @param remote1 25 | * @param session1 connected session 26 | * @return an instance 27 | */ 28 | def Connection(Remote remote1, Session session1) { 29 | remote = remote1 30 | session = session1 31 | assert remote 32 | assert session 33 | } 34 | 35 | /** 36 | * Create an execution channel. 37 | * 38 | * @return a channel 39 | */ 40 | ChannelExec createExecutionChannel() { 41 | session.openChannel('exec') as ChannelExec 42 | } 43 | 44 | /** 45 | * Create a shell channel. 46 | * 47 | * @param operationSettings 48 | * @return a channel 49 | */ 50 | ChannelShell createShellChannel() { 51 | session.openChannel('shell') as ChannelShell 52 | } 53 | 54 | /** 55 | * Create a SFTP channel. 56 | * 57 | * @return a channel 58 | */ 59 | ChannelSftp createSftpChannel() { 60 | session.openChannel('sftp') as ChannelSftp 61 | } 62 | 63 | /** 64 | * Set up local port forwarding. 65 | * 66 | * @param settings 67 | * @return local port 68 | */ 69 | int forwardLocalPort(LocalPortForwardSettings settings) { 70 | session.setPortForwardingL(settings.bind, settings.port, settings.host, settings.hostPort) 71 | } 72 | 73 | /** 74 | * Set up remote port forwarding. 75 | * 76 | * @param settings 77 | */ 78 | void forwardRemotePort(RemotePortForwardSettings settings) { 79 | session.setPortForwardingR(settings.bind, settings.port, settings.host, settings.hostPort) 80 | } 81 | 82 | /** 83 | * Cleanup the connection and all channels. 84 | */ 85 | void close() { 86 | session.disconnect() 87 | log.info("Disconnected from $remote") 88 | } 89 | 90 | @Override 91 | String toString() { 92 | "Connection[$remote]" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/connection/ConnectionSettings.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.connection 2 | 3 | import groovy.transform.EqualsAndHashCode 4 | import org.hidetake.groovy.ssh.core.Remote 5 | import org.hidetake.groovy.ssh.core.settings.SettingsHelper 6 | import org.hidetake.groovy.ssh.core.settings.ToStringProperties 7 | 8 | /** 9 | * Settings for establishing the SSH connection. 10 | * 11 | * @author Hidetake Iwata 12 | */ 13 | trait ConnectionSettings implements UserAuthenticationSettings, HostAuthenticationSettings, ProxyConnectionSettings { 14 | /** 15 | * Gateway host. 16 | * This may be null. 17 | */ 18 | Remote gateway 19 | 20 | /** 21 | * Both connection timeout and socket read timeout in seconds. 22 | */ 23 | Integer timeoutSec 24 | 25 | /** 26 | * Retry count for connecting to a host. 27 | */ 28 | Integer retryCount 29 | 30 | /** 31 | * Interval time in seconds between retries. 32 | */ 33 | Integer retryWaitSec 34 | 35 | /** 36 | * Interval time in seconds between keep-alive packets. 37 | */ 38 | Integer keepAliveSec 39 | 40 | 41 | @EqualsAndHashCode 42 | static class With implements ConnectionSettings, ToStringProperties { 43 | def With() {} 44 | def With(ConnectionSettings... sources) { 45 | SettingsHelper.mergeProperties(this, sources) 46 | } 47 | 48 | static final ConnectionSettings DEFAULT = new ConnectionSettings.With( 49 | user: null, 50 | authentications: ['publickey', 'keyboard-interactive', 'password'], 51 | password: null, 52 | identity: null, 53 | passphrase: null, 54 | gateway: null, 55 | proxy: null, 56 | agent: false, 57 | knownHosts: new File("${System.properties['user.home']}/.ssh/known_hosts"), 58 | timeoutSec: 0, 59 | retryCount: 0, 60 | retryWaitSec: 0, 61 | keepAliveSec: 60, 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/connection/HostAuthenticationSettings.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.connection 2 | 3 | trait HostAuthenticationSettings { 4 | 5 | /** 6 | * Known hosts file. 7 | * This can be a {@link File}, {@link List} or {@link #allowAnyHosts}. 8 | */ 9 | def knownHosts 10 | 11 | /** 12 | * Represents that strict host key checking is turned off and any host is allowed. 13 | * @see #knownHosts 14 | */ 15 | final allowAnyHosts = AllowAnyHosts.instance 16 | 17 | /** 18 | * Represents that a host key is automatically appended to the known hosts file. 19 | * @param knownHostsFile 20 | * @return 21 | * @see #knownHosts 22 | */ 23 | AddHostKey addHostKey(File knownHostsFile) { 24 | new AddHostKey(knownHostsFile) 25 | } 26 | 27 | /** 28 | * Hides constant from result of {@link #toString()}. 29 | */ 30 | def toString__allowAnyHosts() {} 31 | 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/connection/HostKeyRepository.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.connection 2 | 3 | import com.jcraft.jsch.HostKey 4 | import com.jcraft.jsch.HostKeyRepository as JSchHostKeyRepository 5 | import com.jcraft.jsch.JSch 6 | import com.jcraft.jsch.Session 7 | import groovy.util.logging.Slf4j 8 | 9 | import javax.crypto.Mac 10 | import javax.crypto.spec.SecretKeySpec 11 | 12 | /** 13 | * A thin wrapper of {@link com.jcraft.jsch.HostKeyRepository}. 14 | * 15 | * @author Hidetake Iwata 16 | */ 17 | @Slf4j 18 | class HostKeyRepository { 19 | 20 | private final JSchHostKeyRepository hostKeyRepository 21 | 22 | def HostKeyRepository(JSchHostKeyRepository hostKeyRepository1) { 23 | hostKeyRepository = hostKeyRepository1 24 | } 25 | 26 | Collection findAll() { 27 | hostKeyRepository.hostKey.toList() 28 | } 29 | 30 | Collection findAll(String host, int port) { 31 | hostKeyRepository.hostKey.findAll { hostKey -> compare(hostKey, host, port) } 32 | } 33 | 34 | void addAll(Collection hostKeys) { 35 | hostKeys.each { hostKey -> 36 | hostKeyRepository.add(hostKey, null) 37 | } 38 | } 39 | 40 | static HostKeyRepository create(Session session) { 41 | new HostKeyRepository(session.hostKeyRepository) 42 | } 43 | 44 | static HostKeyRepository create(File knownHostsFile) { 45 | def jsch = new JSch() 46 | jsch.setKnownHosts(knownHostsFile.path) 47 | new HostKeyRepository(jsch.hostKeyRepository) 48 | } 49 | 50 | static HostKey translateHostPort(HostKey hostKey, String host, int port) { 51 | if (port == 22) { 52 | new HostKey(host, hostKey.@type, hostKey.@key, hostKey.comment) 53 | } else { 54 | new HostKey("[$host]:$port", hostKey.@type, hostKey.@key, hostKey.comment) 55 | } 56 | } 57 | 58 | static boolean compare(HostKey hostKey, String host, int port) { 59 | String hostAndPort = port == 22 ? host : "[$host]:$port" 60 | if ( hostKey.host.startsWith( '|' ) ) { 61 | return compareHashed( hostKey, hostAndPort ) 62 | } else { 63 | return hostKey.host.split( ',' ).contains( hostAndPort ) 64 | } 65 | } 66 | 67 | private static boolean compareHashed(HostKey hostKey, String host) { 68 | def matcher = (~/^\|1\|(.+?)\|(.+?)$/).matcher(hostKey.host) 69 | if (matcher) { 70 | def salt = matcher.group(1) 71 | def hash = matcher.group(2) 72 | hmacSha1(salt.decodeBase64(), host.bytes) == hash.decodeBase64() 73 | } else { 74 | false 75 | } 76 | } 77 | 78 | private static byte[] hmacSha1(byte[] salt, byte[] data) { 79 | def key = new SecretKeySpec(salt, 'HmacSHA1') 80 | def mac = Mac.getInstance(key.algorithm) 81 | mac.init(key) 82 | mac.doFinal(data) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/connection/JSchLogger.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.connection 2 | 3 | import com.jcraft.jsch.JSch 4 | import com.jcraft.jsch.Logger 5 | import groovy.util.logging.Slf4j 6 | 7 | /** 8 | * A logger which bridges JSch and SLF4J. 9 | * It does not redirect DEBUG log because it is too detail and verbose. 10 | * 11 | * @author Hidetake Iwata 12 | */ 13 | @Singleton 14 | @Slf4j 15 | class JSchLogger implements Logger { 16 | private static final ThreadLocal enabledInCurrentThread = new ThreadLocal<>() 17 | 18 | static void setEnabledInCurrentThread(boolean enabled) { 19 | JSch.logger = JSchLogger.instance 20 | enabledInCurrentThread.set(enabled) 21 | log.debug("${enabled ? 'Enabled' : 'Disabled'} JSch logging on ${Thread.currentThread()}") 22 | } 23 | 24 | @Override 25 | boolean isEnabled(int logLevel) { 26 | if (enabledInCurrentThread.get()) { 27 | switch (logLevel) { 28 | case INFO: return log.isDebugEnabled() 29 | case WARN: return log.isInfoEnabled() 30 | case ERROR: return log.isWarnEnabled() 31 | case FATAL: return log.isErrorEnabled() 32 | default: return false 33 | } 34 | } else { 35 | false 36 | } 37 | } 38 | 39 | @Override 40 | void log(int logLevel, String message) { 41 | switch (logLevel) { 42 | case INFO: 43 | log.debug("[jsch] $message") 44 | break 45 | case WARN: 46 | log.info("[jsch] $message") 47 | break 48 | case ERROR: 49 | log.warn("[jsch] $message") 50 | break 51 | case FATAL: 52 | log.error("[jsch] $message") 53 | break 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/connection/ProxyConnection.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.connection 2 | 3 | import com.jcraft.jsch.* 4 | import groovy.util.logging.Slf4j 5 | import org.hidetake.groovy.ssh.core.Remote 6 | 7 | import static org.hidetake.groovy.ssh.core.ProxyType.SOCKS 8 | 9 | @Slf4j 10 | trait ProxyConnection { 11 | 12 | void validateProxyConnection(ProxyConnectionSettings settings, Remote remote) { 13 | if (settings.proxy) { 14 | def validator = new ProxyValidator(settings.proxy) 15 | if (validator.error()) { 16 | throw new IllegalArgumentException(validator.error()) 17 | } 18 | if (validator.warnings()) { 19 | validator.warnings().each { warning -> log.info(warning) } 20 | } 21 | } 22 | } 23 | 24 | void configureProxyConnection(JSch jsch, Session session, Remote remote, ProxyConnectionSettings settings) { 25 | if (settings.proxy) { 26 | if (settings.proxy.type == SOCKS) { 27 | if (settings.proxy.socksVersion == 5) { 28 | def proxy = new ProxySOCKS5(settings.proxy.host, settings.proxy.port) 29 | proxy.setUserPasswd(settings.proxy.user, settings.proxy.password) 30 | session.proxy = proxy 31 | log.debug("Using SOCKS5 proxy for $remote: $settings.proxy") 32 | } else { 33 | def proxy = new ProxySOCKS4(settings.proxy.host, settings.proxy.port) 34 | proxy.setUserPasswd(settings.proxy.user, settings.proxy.password) 35 | session.proxy = proxy 36 | log.debug("Using SOCKS4 proxy for $remote: $settings.proxy") 37 | } 38 | } else { 39 | def proxy = new ProxyHTTP(settings.proxy.host, settings.proxy.port) 40 | proxy.setUserPasswd(settings.proxy.user, settings.proxy.password) 41 | session.proxy = proxy 42 | log.debug("Using HTTP proxy for $remote: $settings.proxy") 43 | } 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/connection/ProxyConnectionSettings.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.connection 2 | 3 | import org.hidetake.groovy.ssh.core.Proxy 4 | 5 | trait ProxyConnectionSettings { 6 | 7 | /** 8 | * Proxy configuration for connecting to a host. 9 | * This may be null. 10 | */ 11 | Proxy proxy 12 | 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/connection/ProxyValidator.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.connection 2 | 3 | import org.hidetake.groovy.ssh.core.Proxy 4 | import org.hidetake.groovy.ssh.core.ProxyType 5 | 6 | import static org.hidetake.groovy.ssh.core.ProxyType.SOCKS 7 | 8 | /** 9 | * Basic validation and defaults for proxied connections created by {@link ConnectionManager}. 10 | * 11 | * @author mlipper 12 | * 13 | */ 14 | class ProxyValidator { 15 | protected static final SOCKS_DEFAULT_VERSION = 5 16 | protected static final SOCKS_SUPPORTED_VERSIONS = 4..5 17 | 18 | private final Proxy proxy 19 | private final Map report 20 | 21 | ProxyValidator(Proxy proxy1) { 22 | this.proxy = proxy1 23 | this.report = [error:null,warnings:[]] 24 | createReport() 25 | } 26 | 27 | String error() { report.error } 28 | 29 | List warnings() { report.warnings ?: null } 30 | 31 | private void createReport() { 32 | validateProxyType() 33 | ensureSocksVersion() 34 | checkCredentials() 35 | } 36 | 37 | private void validateProxyType() { 38 | if(!ProxyType.values().contains(proxy.type)) { 39 | report.error = "Unsupported ProxyType ${proxy.type}. Supported types: ${ProxyType.collect {"$it"}.join(', ')}." 40 | } 41 | } 42 | 43 | private void checkCredentials() { 44 | // DefaultConnectionManager ignores authentication credentials when 45 | // creating proxy server connections unless both proxy.user and 46 | // proxy.password are set 47 | if(proxy.user && !proxy.password) { 48 | addWarning("proxy.user is set but proxy.password is null. Credentials are ignored for proxy '${proxy.name}'") 49 | } 50 | if(!proxy.user && proxy.password) { 51 | addWarning("proxy.password is set but proxy.user is null. Credentials are ignored for proxy '${proxy.name}'") 52 | } 53 | } 54 | 55 | private void ensureSocksVersion() { 56 | def v = proxy.socksVersion 57 | if(SOCKS == proxy.type && !SOCKS_SUPPORTED_VERSIONS.contains(v)) { 58 | if(v == 0) { 59 | addWarning("Using SOCKS v$SOCKS_DEFAULT_VERSION since proxy.socksVersion is not set.") 60 | } else { 61 | addWarning("Using SOCKS v$SOCKS_DEFAULT_VERSION since proxy.socksVersion is set to ${proxy.socksVersion} which is not supported by this implementation.") 62 | } 63 | proxy.socksVersion = SOCKS_DEFAULT_VERSION 64 | } 65 | } 66 | 67 | private void addWarning(String message) { 68 | report.warnings.add(message) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/connection/UserAuthentication.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.connection 2 | 3 | import org.hidetake.groovy.ssh.core.Remote 4 | 5 | import com.jcraft.jsch.AgentIdentityRepository 6 | import com.jcraft.jsch.IdentityRepository 7 | import com.jcraft.jsch.JSch 8 | import com.jcraft.jsch.SSHAgentConnector 9 | import com.jcraft.jsch.Session 10 | 11 | import groovy.util.logging.Slf4j 12 | 13 | @Slf4j 14 | trait UserAuthentication { 15 | 16 | void validateUserAuthentication(UserAuthenticationSettings settings, Remote remote) { 17 | assert settings.user, "user must be given ($remote)" 18 | assert settings.identity instanceof File || settings.identity instanceof String || settings.identity == null, 19 | "identity must be a File, String or null ($remote)" 20 | } 21 | 22 | void configureUserAuthentication(JSch jsch, Session session, Remote remote, UserAuthenticationSettings settings) { 23 | session.setConfig('PreferredAuthentications', settings.authentications.join(',')) 24 | 25 | if (settings.password) { 26 | session.password = settings.password 27 | log.debug("Using password authentication for $remote") 28 | } 29 | 30 | if (settings.agent) { 31 | // Use agent authentication using https://github.com/mwiede/jsch/issues/65#issuecomment-913051572 32 | IdentityRepository irepo = new AgentIdentityRepository(new SSHAgentConnector()) 33 | jsch.identityRepository = irepo 34 | log.debug("Using SSH agent authentication for $remote") 35 | } else { 36 | jsch.identityRepository = null /* null means the default repository */ 37 | jsch.removeAllIdentity() 38 | if (settings.identity) { 39 | final identity = settings.identity 40 | if (identity instanceof File) { 41 | jsch.addIdentity(identity.path, settings.passphrase as String) 42 | log.debug("Using public key authentication for $remote: $identity.path") 43 | } else if (identity instanceof String) { 44 | jsch.addIdentity("identity-${identity.hashCode()}", identity.bytes, null, settings.passphrase?.bytes) 45 | log.debug("Using public key authentication for $remote") 46 | } 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/connection/UserAuthenticationSettings.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.connection 2 | 3 | trait UserAuthenticationSettings { 4 | /** 5 | * Remote user. 6 | */ 7 | String user 8 | 9 | /** 10 | * Authentication methods. 11 | */ 12 | List authentications 13 | 14 | /** 15 | * Password. 16 | * Leave as null if the password authentication is not needed. 17 | */ 18 | String password 19 | 20 | /** 21 | * Hides credential from result of {@link #toString()}. 22 | */ 23 | def toString__password() { '...' } 24 | 25 | /** 26 | * Identity key file for public-key authentication. 27 | * This must be a {@link File}, {@link String} or null. 28 | * Leave as null if the public key authentication is not needed. 29 | */ 30 | def identity 31 | 32 | /** 33 | * {@link #toString()} formatter to hide credential. 34 | */ 35 | def toString__identity() { identity instanceof File ? identity : '...' } 36 | 37 | /** 38 | * Pass-phrase for the identity key. 39 | * This may be null. 40 | */ 41 | String passphrase 42 | 43 | /** 44 | * Hides credential from result of {@link #toString()}. 45 | */ 46 | def toString__passphrase() { '...' } 47 | 48 | def plus__passphrase(UserAuthenticationSettings prior) { 49 | if (prior.identity == null) { 50 | if (identity == null) { 51 | null 52 | } else { 53 | passphrase 54 | } 55 | } else { 56 | prior.passphrase 57 | } 58 | } 59 | 60 | /** 61 | * Use agent flag. 62 | * If true, Putty Agent or ssh-agent will be used to authenticate. 63 | */ 64 | Boolean agent 65 | } 66 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/core/ParallelSessionsException.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core 2 | 3 | import java.util.concurrent.ForkJoinTask 4 | 5 | /** 6 | * An exception for parallel sessions. 7 | * 8 | * @author Hidetake Iwata 9 | */ 10 | class ParallelSessionsException extends Exception { 11 | final List tasks 12 | final List causes 13 | 14 | def ParallelSessionsException(String message, List tasks1) { 15 | super(message, firstCause(tasks1)) 16 | tasks = tasks1 17 | causes = tasks*.exception.findAll() 18 | } 19 | 20 | private static firstCause(List tasks) { 21 | tasks.find { task -> task.completedAbnormally }.exception 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/core/Proxy.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core 2 | 3 | /** 4 | * Represents a connection proxy to use when establishing a {@link org.hidetake.groovy.ssh.connection.Connection}. 5 | * An instance of this class be shared by multiple {@link Remote}s. 6 | * 7 | * @author mlipper 8 | * 9 | */ 10 | class Proxy { 11 | /** 12 | * Adds all type of the {@link ProxyType}, 13 | * in order to omit import in a build script. 14 | */ 15 | static { 16 | ProxyType.values().each { proxyType -> 17 | Proxy.metaClass[proxyType.name()] = proxyType 18 | } 19 | } 20 | 21 | /** 22 | * Name of this instance. 23 | */ 24 | final String name 25 | 26 | def Proxy(String name1) { 27 | name = name1 28 | assert name 29 | } 30 | 31 | /** 32 | * Proxy protocol type 33 | */ 34 | ProxyType type 35 | 36 | /** 37 | * SOCKS protocol version. 38 | * This should be set when using ProxyType.SOCKS. 39 | */ 40 | int socksVersion 41 | 42 | /** 43 | * Port. 44 | */ 45 | int port 46 | 47 | /** 48 | * Proxy host. 49 | */ 50 | String host 51 | 52 | /** 53 | * Proxy user. 54 | * This may be null. 55 | */ 56 | String user 57 | 58 | /** 59 | * Proxy password. 60 | * This may be null. 61 | */ 62 | String password 63 | 64 | String toString() { 65 | "$name [$host:$port]" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/core/ProxyType.java: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core; 2 | 3 | /** 4 | * Proxy type. 5 | * Implemented as Java native enum for Gradle 1.x compatibility. 6 | * 7 | * @author mlipper 8 | * @author Hidetake Iwata 9 | */ 10 | public enum ProxyType { 11 | HTTP, 12 | SOCKS 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/core/Remote.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core 2 | 3 | import groovy.transform.EqualsAndHashCode 4 | import org.hidetake.groovy.ssh.core.settings.CompositeSettings 5 | 6 | import java.util.concurrent.atomic.AtomicInteger 7 | 8 | /** 9 | * Represents a remote host. 10 | * 11 | * @author Hidetake Iwata 12 | */ 13 | @EqualsAndHashCode(includes = 'name') 14 | class Remote implements CompositeSettings { 15 | /** 16 | * Name of this instance. 17 | */ 18 | final String name 19 | 20 | def Remote(String name1) { 21 | name = name1 22 | assert name 23 | } 24 | 25 | def Remote(Map settingsMap) { 26 | name = settingsMap.name ?: "Remote${sequenceForAutoNaming.incrementAndGet()}" 27 | settingsMap.findAll { key, value -> 28 | key != 'name' 29 | }.each { key, value -> 30 | setProperty(key, value) 31 | } 32 | } 33 | 34 | private static final sequenceForAutoNaming = new AtomicInteger() 35 | 36 | /** 37 | * Remote host. 38 | */ 39 | String host 40 | 41 | /** 42 | * Port. 43 | */ 44 | int port = 22 45 | 46 | /** 47 | * Roles. 48 | */ 49 | final Set roles = [] 50 | 51 | /** 52 | * Add the role. 53 | * @param role 54 | */ 55 | void role(String role) { 56 | assert role != null, 'role should be set' 57 | roles.add(role) 58 | } 59 | 60 | String toString() { 61 | "$name[$host:$port]" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/core/RunHandler.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core 2 | 3 | import org.hidetake.groovy.ssh.core.settings.PerServiceSettings 4 | import org.hidetake.groovy.ssh.session.Session 5 | import org.hidetake.groovy.ssh.session.SessionHandler 6 | 7 | import static org.hidetake.groovy.ssh.util.Utility.callWithDelegate 8 | 9 | /** 10 | * A handler of {@link Service#run(groovy.lang.Closure)}. 11 | * 12 | * @author Hidetake Iwata 13 | */ 14 | class RunHandler { 15 | /** 16 | * Per service settings. 17 | */ 18 | final settings = new PerServiceSettings() 19 | 20 | /** 21 | * Sessions added in the closure of {@link Service#run(groovy.lang.Closure)}. 22 | */ 23 | final List sessions = [] 24 | 25 | /** 26 | * Configure per service settings. 27 | * 28 | * @param closure closure for {@link PerServiceSettings} 29 | */ 30 | void settings(@DelegatesTo(PerServiceSettings) Closure closure) { 31 | assert closure, 'closure must be given' 32 | callWithDelegate(closure, settings) 33 | } 34 | 35 | /** 36 | * Add a session. 37 | * 38 | * @param remote the {@link Remote} 39 | * @param closure closure for {@link SessionHandler} 40 | */ 41 | void session(Remote remote, @DelegatesTo(SessionHandler) Closure closure) { 42 | assert remote, 'remote must be given' 43 | assert remote.host, "host must be given ($remote)" 44 | assert closure, 'closure must be given' 45 | sessions.add(new Session(remote, closure)) 46 | } 47 | 48 | /** 49 | * Add sessions. 50 | * 51 | * @param remotes collection of {@link Remote}s 52 | * @param closure closure for {@link SessionHandler} 53 | */ 54 | void session(Collection remotes, @DelegatesTo(SessionHandler) Closure closure) { 55 | assert remotes, 'at least one remote must be given' 56 | remotes.each { remote -> session(remote, closure) } 57 | } 58 | 59 | /** 60 | * Add a session. 61 | * This method creates a {@link Remote} instance and add a session with it. 62 | * 63 | * @param settings settings of a {@link Remote} 64 | * @param closure closure for {@link SessionHandler} 65 | */ 66 | void session(Map settings, @DelegatesTo(SessionHandler) Closure closure) { 67 | assert settings, 'properties of a remote must be given' 68 | session(new Remote(settings), closure) 69 | } 70 | 71 | /** 72 | * Add sessions. 73 | * This is a last resort method and allows only below arguments. 74 | * 75 | * @param args elements except last must be {@link Remote}s and last must be a closure 76 | * @throws IllegalArgumentException if wrong arguments are given. 77 | */ 78 | void session(Object[] args) { 79 | if (args.last() instanceof Closure) { 80 | def remotes = args.take(args.length - 1) as Collection 81 | def closure = args.last() as Closure 82 | session(remotes, closure) 83 | } else { 84 | throw new IllegalArgumentException('''session() allows following arguments: 85 | session(remote) {} 86 | session(remote1, remote2, ...) {} 87 | session([remote1, remote2, ...]) {} 88 | session(host: 'myHost', user: 'myUser', ...) {}''') 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/core/container/Container.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core.container 2 | 3 | import static org.hidetake.groovy.ssh.util.Utility.callWithDelegate 4 | 5 | /** 6 | * A container. 7 | * 8 | * @author Hidetake Iwata 9 | */ 10 | trait Container implements Map { 11 | /** 12 | * Add an item. 13 | * The item must have name property. 14 | * If this map already contains an item with same name, it will be overwritten. 15 | * 16 | * @param item 17 | * @return true if this map did not already contain the same name 18 | */ 19 | boolean add(T item) { 20 | assert item.name instanceof String 21 | put(item.name as String, item) ? false : true 22 | } 23 | 24 | /** 25 | * Add items. 26 | * Each item must have name property. 27 | * If this map already contains an item with same name, it will be overwritten. 28 | * 29 | * @param items 30 | */ 31 | void addAll(Collection items) { 32 | putAll(items.collectEntries { item -> 33 | assert item.name instanceof String 34 | [(item.name): item] 35 | }) 36 | } 37 | 38 | /** 39 | * Create an item and add it. 40 | * If this map already contains an item with same name, it will be overwritten. 41 | * 42 | * @param name 43 | * @param closure 44 | * @return item 45 | */ 46 | T create(String name, Closure closure) { 47 | assert name 48 | assert getContainerElementType() instanceof Class 49 | T namedObject = getContainerElementType().newInstance(name) 50 | callWithDelegate(closure, namedObject) 51 | add(namedObject) 52 | namedObject 53 | } 54 | 55 | /** 56 | * Type of the container element. 57 | */ 58 | abstract Class getContainerElementType() 59 | } 60 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/core/container/ContainerBuilder.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core.container 2 | 3 | /** 4 | * An evaluator of a closure which contains named objects. 5 | * 6 | * @param < T > type of the named object 7 | * 8 | * @author Hidetake Iwata 9 | */ 10 | class ContainerBuilder { 11 | private final Container container 12 | 13 | def ContainerBuilder(Container container1) { 14 | container = container1 15 | } 16 | 17 | T methodMissing(String name, args) { 18 | assert args instanceof Object[] 19 | 20 | try { 21 | assert args.length == 1 22 | 23 | def configurationClosure = args[0] 24 | assert configurationClosure instanceof Closure 25 | 26 | def containerClosure = configurationClosure.delegate 27 | assert containerClosure instanceof Closure 28 | 29 | def delegateOfContainerClosure = containerClosure.delegate 30 | assert !container.getContainerElementType().isInstance(delegateOfContainerClosure) 31 | } catch (AssertionError ignore) { 32 | throw new MissingMethodException(name, ContainerBuilder, args) 33 | } 34 | 35 | container.create(name, args[0] as Closure) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/core/container/ProxyContainer.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core.container 2 | 3 | import org.hidetake.groovy.ssh.core.Proxy 4 | 5 | import java.util.concurrent.ConcurrentSkipListMap 6 | 7 | /** 8 | * A container of proxies. 9 | * 10 | * @author Hidetake Iwata 11 | */ 12 | class ProxyContainer extends ConcurrentSkipListMap implements Container { 13 | final Class containerElementType = Proxy 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/core/container/RemoteContainer.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core.container 2 | 3 | import org.hidetake.groovy.ssh.core.Remote 4 | 5 | import java.util.concurrent.ConcurrentSkipListMap 6 | 7 | /** 8 | * A container of remote hosts. 9 | * 10 | * @author Hidetake Iwata 11 | */ 12 | class RemoteContainer extends ConcurrentSkipListMap implements Container, RoleAccessible { 13 | final Class containerElementType = Remote 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/core/container/RoleAccessible.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core.container 2 | 3 | import org.hidetake.groovy.ssh.core.Remote 4 | 5 | /** 6 | * Provides role access for remote hosts. 7 | * 8 | * @author Hidetake Iwata 9 | */ 10 | trait RoleAccessible { 11 | /** 12 | * Find remote hosts associated with given roles. 13 | * 14 | * @param roles one or more roles 15 | * @return remote hosts associated with given roles 16 | */ 17 | Collection role(String... roles) { 18 | assert roles, 'At least one role must be given' 19 | getAsRemoteCollection().findAll { it.roles.any { it in roles } } 20 | } 21 | 22 | /** 23 | * Find remote hosts associated with all given roles. 24 | * 25 | * @param roles one or more roles 26 | * @return remote hosts associated with all given roles 27 | */ 28 | Collection allRoles(String... roles) { 29 | assert roles, 'At least one role must be given' 30 | getAsRemoteCollection().findAll { it.roles.containsAll(roles) } 31 | } 32 | 33 | private Collection getAsRemoteCollection() { 34 | if (this instanceof Map) { 35 | (this as Map).values() 36 | } else { 37 | (this as Collection) 38 | } 39 | } 40 | 41 | static interface RoleAccessor { 42 | /** 43 | * Find remote hosts associated with given role. 44 | * 45 | * @param name a role 46 | * @return remote hosts associated with given roles 47 | */ 48 | Collection getAt(String name) 49 | } 50 | 51 | final RoleAccessor role = [getAt: { String name -> role(name) }] as RoleAccessor 52 | } 53 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/core/settings/CompositeSettings.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core.settings 2 | 3 | import org.hidetake.groovy.ssh.connection.ConnectionSettings 4 | import org.hidetake.groovy.ssh.operation.CommandSettings 5 | import org.hidetake.groovy.ssh.operation.ShellSettings 6 | import org.hidetake.groovy.ssh.session.SessionSettings 7 | import org.hidetake.groovy.ssh.session.execution.SudoSettings 8 | import org.hidetake.groovy.ssh.session.transfer.FileTransferSettings 9 | 10 | /** 11 | * Represents overall settings configurable in 12 | * {@link org.hidetake.groovy.ssh.core.Service#settings} and 13 | * {@link org.hidetake.groovy.ssh.core.RunHandler#settings}. 14 | * 15 | * @author Hidetake Iwata 16 | */ 17 | trait CompositeSettings implements 18 | ConnectionSettings, 19 | SessionSettings, 20 | CommandSettings, 21 | ShellSettings, 22 | SudoSettings, 23 | FileTransferSettings 24 | { 25 | static class With implements CompositeSettings, ToStringProperties { 26 | def With() {} 27 | def With(CompositeSettings... sources) { 28 | SettingsHelper.mergeProperties(this, sources) 29 | } 30 | 31 | static final CompositeSettings DEFAULT = new CompositeSettings.With() 32 | static { 33 | SettingsHelper.mergeProperties(DEFAULT, 34 | ConnectionSettings.With.DEFAULT, 35 | SessionSettings.With.DEFAULT, 36 | CommandSettings.With.DEFAULT, 37 | ShellSettings.With.DEFAULT, 38 | SudoSettings.With.DEFAULT, 39 | FileTransferSettings.With.DEFAULT, 40 | ) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/core/settings/GlobalSettings.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core.settings 2 | 3 | /** 4 | * A global settings. 5 | * 6 | * @author Hidetake Iwata 7 | */ 8 | class GlobalSettings extends CompositeSettings.With { 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/core/settings/LoggingMethod.java: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core.settings; 2 | 3 | /** 4 | * Logging method type. 5 | * Implemented as Java native enum for Gradle 1.x compatibility. 6 | * 7 | * @author Hidetake Iwata 8 | */ 9 | public enum LoggingMethod { 10 | /** 11 | * Log is sent to SLF4J. 12 | */ 13 | slf4j, 14 | 15 | /** 16 | * Log is sent to standard output or error. 17 | */ 18 | stdout, 19 | 20 | /** 21 | * Logging is turned off. 22 | */ 23 | none 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/core/settings/PerServiceSettings.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core.settings 2 | 3 | /** 4 | * Per-service settings. 5 | * 6 | * @author Hidetake Iwata 7 | */ 8 | class PerServiceSettings extends CompositeSettings.With { 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/core/settings/ToStringProperties.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core.settings 2 | 3 | /** 4 | * A trait for overriding {@link Object#toString()} to show properties of this object. 5 | * This may help debug by user friendly log. 6 | * 7 | * @author Hidetake Iwata 8 | */ 9 | trait ToStringProperties { 10 | /** 11 | * Returns a string representation of this settings. 12 | * Class should implements method toString__propertyName 13 | * to exclude or customize property representation. 14 | * See test for details of specification. 15 | * 16 | * @returns string representation of this settings 17 | */ 18 | @Override 19 | String toString() { 20 | '{' + SettingsHelper.computePropertiesToString(this).collect { key, value -> 21 | "$key=${value.toString()}" 22 | }.join(', ') + '}' 23 | } 24 | } -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/core/type/InputStreamValue.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core.type 2 | 3 | /** 4 | * A value object for 5 | * {@link org.hidetake.groovy.ssh.operation.CommandSettings#inputStream}, 6 | * {@link org.hidetake.groovy.ssh.operation.ShellSettings#inputStream}, 7 | * {@link org.hidetake.groovy.ssh.session.execution.SudoSettings#inputStream}. 8 | * 9 | * @author Hidetake Iwata 10 | */ 11 | class InputStreamValue { 12 | 13 | private final value 14 | 15 | def InputStreamValue(def value1) { 16 | if (value == null || 17 | value instanceof InputStream || 18 | value instanceof byte[] || 19 | value instanceof String || 20 | value instanceof File) { 21 | value = value1 22 | } else { 23 | throw new IllegalArgumentException("inputStream must be InputStream, byte[], String or File: $value1") 24 | } 25 | } 26 | 27 | boolean asBoolean() { 28 | value != null 29 | } 30 | 31 | InputStreamValue rightShift(OutputStream stream) { 32 | if (value instanceof InputStream) { 33 | stream << value 34 | } else if (value instanceof byte[]) { 35 | stream << value 36 | } else if (value instanceof String) { 37 | stream << value 38 | } else if (value instanceof File) { 39 | value.withInputStream { 40 | stream << it 41 | } 42 | } 43 | this 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/interaction/Buffer.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.interaction 2 | 3 | import groovy.util.logging.Slf4j 4 | 5 | /** 6 | * A byte buffer which supports last-in and first-out. 7 | * 8 | * @author Hidetake Iwata 9 | */ 10 | @Slf4j 11 | class Buffer { 12 | 13 | private final String encoding 14 | 15 | private final buffer = new ByteArrayOutputStream(Receiver.READ_BUFFER_SIZE) 16 | 17 | def Buffer(String encoding1) { 18 | encoding = encoding1 19 | } 20 | 21 | /** 22 | * Append bytes to last. 23 | * 24 | * @param bytes 25 | * @param length 26 | */ 27 | void append(byte[] bytes, int length) { 28 | buffer.write(bytes, 0, length) 29 | log.trace("Appended $length bytes to buffer, now ${size()} bytes") 30 | } 31 | 32 | /** 33 | * Append bytes to last. 34 | * 35 | * @param bytes 36 | */ 37 | void append(byte[] bytes) { 38 | append(bytes, bytes.length) 39 | } 40 | 41 | /** 42 | * Append string to last. 43 | * 44 | * @param string 45 | */ 46 | void append(String string) { 47 | append(string.getBytes(encoding)) 48 | } 49 | 50 | /** 51 | * Drop first bytes. 52 | * If {@param length} is greater than buffer size, whole is dropped. 53 | * 54 | * @param length must be greater than 0 55 | * @return dropped bytes 56 | */ 57 | byte[] dropBytes(int length) { 58 | assert length > 0, 'can not drop 0 or minus byte' 59 | def original = buffer.toByteArray() 60 | buffer.reset() 61 | if (length < original.length) { 62 | buffer.write(original, length, original.length - length) 63 | log.trace("Dropped first $length bytes of buffer, now ${size()} bytes") 64 | Arrays.copyOf(original, length) 65 | } else { 66 | log.trace("Dropped whole buffer of $original.length bytes (requested $length bytes)") 67 | original 68 | } 69 | } 70 | 71 | /** 72 | * Drop first bytes of given characters length. 73 | * 74 | * @param string 75 | */ 76 | void dropChars(String string) { 77 | def length = string.getBytes(encoding).length 78 | assert length > 0, 'can not drop 0 byte' 79 | def original = buffer.toByteArray() 80 | buffer.reset() 81 | if (length < original.length) { 82 | buffer.write(original, length, original.length - length) 83 | log.trace("Dropped first $length bytes of buffer, now ${size()} bytes") 84 | } else { 85 | log.trace("Dropped whole buffer of $original.length bytes (requested $length bytes)") 86 | } 87 | } 88 | 89 | /** 90 | * Return size of the buffer. 91 | * @return 92 | */ 93 | int size() { 94 | buffer.size() 95 | } 96 | 97 | /** 98 | * Return string representation of the buffer. 99 | * @return 100 | */ 101 | String toString() { 102 | buffer.toString(encoding) 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/interaction/Context.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.interaction 2 | 3 | import groovy.util.logging.Slf4j 4 | 5 | /** 6 | * A context class of stream interaction. 7 | * This should be pushed into the stack and replaced on rule matched. 8 | * 9 | * @author Hidetake Iwata 10 | */ 11 | @Slf4j 12 | class Context { 13 | 14 | private final List rules 15 | 16 | def Context(List rules1) { 17 | rules = rules1 18 | } 19 | 20 | MatchResult match(Stream stream, Buffer buffer) { 21 | rules.findResult { rule -> 22 | rule.match(stream, buffer) 23 | } 24 | } 25 | 26 | @Override 27 | String toString() { 28 | "${Context.simpleName}$rules" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/interaction/InteractionException.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.interaction 2 | 3 | /** 4 | * An exception thrown if one or more exceptions occurred while stream interaction. 5 | * 6 | * @author Hidetake Iwata 7 | */ 8 | class InteractionException extends IOException { 9 | final List exceptions 10 | 11 | def InteractionException(Throwable exception) { 12 | super("Error while stream interaction: $exception", exception) 13 | this.exceptions = [exception] 14 | } 15 | 16 | def InteractionException(Throwable... exceptions) { 17 | super("Error while stream interaction: $exceptions") 18 | this.exceptions = exceptions 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/interaction/InteractionHandler.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.interaction 2 | 3 | /** 4 | * A handler of the interaction closure. 5 | * 6 | * @author Hidetake Iwata 7 | */ 8 | class InteractionHandler { 9 | /** 10 | * Wildcard for condition expression. 11 | */ 12 | static final _ = Wildcard.instance 13 | 14 | static final standardOutput = Stream.StandardOutput 15 | 16 | static final standardError = Stream.StandardError 17 | 18 | /** 19 | * A standard input for the remote command. 20 | */ 21 | final OutputStream standardInput 22 | 23 | final List when = [] 24 | boolean popContext = false 25 | 26 | def InteractionHandler(OutputStream standardInput1) { 27 | standardInput = standardInput1 28 | assert standardInput 29 | } 30 | 31 | /** 32 | * Declare an interaction rule. 33 | * 34 | * @param condition see {@link StreamRule} and {@link BufferRule} for details 35 | * @param action closure called with result ({@link String}, {@link java.util.regex.Matcher} or {@code byte[]}) when condition is satisfied 36 | */ 37 | void when(Map condition, Closure action) { 38 | assert condition, 'at least one rule must be given' 39 | assert action, 'closure must be given' 40 | when.add(new Rule(condition, action)) 41 | } 42 | 43 | /** 44 | * Pop context stack of {@link Processor}. 45 | * This should not be used with {@link #when(java.util.Map, groovy.lang.Closure)}. 46 | */ 47 | void popContext() { 48 | popContext = true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/interaction/Interactions.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.interaction 2 | 3 | import groovy.util.logging.Slf4j 4 | 5 | /** 6 | * An aggregation class of streams and receiver threads. 7 | * 8 | * @author Hidetake Iwata 9 | */ 10 | @Slf4j 11 | class Interactions { 12 | private final OutputStream standardInput 13 | private final String encoding 14 | 15 | private final Listener listener = new Listener() 16 | private final List receivers = [] 17 | private final List threads = [] 18 | private final List exceptions = [].asSynchronized() 19 | 20 | private final uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() { 21 | @Override 22 | void uncaughtException(Thread thread, Throwable throwable) { 23 | log.debug("Uncaught exception at $thread", throwable) 24 | exceptions.add(throwable) 25 | threads*.interrupt() 26 | } 27 | } 28 | 29 | /** 30 | * Constructor. 31 | * All streams will be closed by the receiver thread. 32 | * 33 | * @param standardInput1 34 | * @param standardOutput 35 | * @param standardError 36 | * @param encoding 37 | * @return 38 | */ 39 | def Interactions(OutputStream standardInput1, InputStream standardOutput, InputStream standardError, String encoding1) { 40 | standardInput = standardInput1 41 | encoding = encoding1 42 | receivers.add(new Receiver(listener, Stream.StandardOutput, standardOutput)) 43 | receivers.add(new Receiver(listener, Stream.StandardError, standardError)) 44 | } 45 | 46 | /** 47 | * Constructor. 48 | * All streams will be closed by the receiver thread. 49 | * 50 | * @param standardInput1 51 | * @param standardOutput 52 | * @param encoding 53 | * @return 54 | */ 55 | def Interactions(OutputStream standardInput1, InputStream standardOutput, String encoding1) { 56 | standardInput = standardInput1 57 | encoding = encoding1 58 | receivers.add(new Receiver(listener, Stream.StandardOutput, standardOutput)) 59 | } 60 | 61 | /** 62 | * Pipes the stream into another stream. 63 | * 64 | * @param stream 65 | * @param outputStream 66 | */ 67 | void pipe(Stream stream, OutputStream outputStream) { 68 | receivers.find { it.stream == stream }.pipes.add(outputStream) 69 | } 70 | 71 | /** 72 | * Adds an interaction. 73 | * 74 | * @param closure definition of interaction 75 | */ 76 | void add(@DelegatesTo(InteractionHandler) Closure closure) { 77 | listener.add(new Processor(closure, standardInput, encoding)) 78 | } 79 | 80 | /** 81 | * Starts receiver threads. 82 | */ 83 | void start() { 84 | threads.addAll(receivers.collect { new Thread(it, it.toString()) }) 85 | threads*.uncaughtExceptionHandler = uncaughtExceptionHandler 86 | 87 | exceptions.clear() 88 | 89 | threads*.start() 90 | } 91 | 92 | /** 93 | * Waits for all receiver threads. 94 | */ 95 | void waitForEndOfStream() { 96 | log.debug("Waiting for interaction threads: $threads") 97 | threads*.join() 98 | log.debug("Terminated interaction threads: $threads") 99 | if (!exceptions.empty) { 100 | throw new InteractionException(*exceptions) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/interaction/Listener.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.interaction 2 | 3 | import groovy.util.logging.Slf4j 4 | 5 | /** 6 | * A listener of lines and partial strings from streams. 7 | * It notifies events to {@link Processor}s on start, received bytes and end of stream. 8 | * 9 | * @author Hidetake Iwata 10 | */ 11 | @Slf4j 12 | class Listener { 13 | private final List processors = [] 14 | 15 | void add(Processor processor) { 16 | processors.add(processor) 17 | } 18 | 19 | void start(Stream stream) { 20 | processors*.start(stream) 21 | } 22 | 23 | void receive(Stream stream, byte[] bytes, int length) { 24 | processors*.receive(stream, bytes, length) 25 | } 26 | 27 | void end(Stream stream) { 28 | processors*.end(stream) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/interaction/MatchResult.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.interaction 2 | 3 | import java.util.regex.Matcher 4 | 5 | class MatchResult { 6 | 7 | private final Rule rule 8 | private final E result 9 | 10 | def MatchResult(Rule rule1, E result1) { 11 | rule = rule1 12 | result = result1 13 | } 14 | 15 | def getActionWithResult() { 16 | rule.action.curry(result) 17 | } 18 | 19 | def getResultAsString() { 20 | if (result instanceof Matcher) { 21 | result.group() 22 | } else if (result instanceof byte[]) { 23 | "byte[$result.length]" 24 | } else { 25 | result.toString() 26 | } 27 | } 28 | 29 | @Override 30 | String toString() { 31 | "$rule -> $resultAsString" 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/interaction/Receiver.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.interaction 2 | 3 | import groovy.util.logging.Slf4j 4 | 5 | import java.util.concurrent.atomic.AtomicInteger 6 | 7 | /** 8 | * A receiver thread reading lines from the stream. 9 | * It notifies events to {@link Listener} on received bytes. 10 | * 11 | * @author Hidetake Iwata 12 | */ 13 | @Slf4j 14 | class Receiver implements Runnable { 15 | static final READ_BUFFER_SIZE = 1024 * 1024 16 | 17 | private static final sequenceForNaming = new AtomicInteger() 18 | 19 | final Stream stream 20 | final List pipes = [] 21 | 22 | private final Listener listener 23 | private final InputStream inputStream 24 | private final int id = sequenceForNaming.incrementAndGet() 25 | 26 | def Receiver(Listener listener1, Stream stream1, InputStream inputStream1) { 27 | listener = listener1 28 | stream = stream1 29 | inputStream = inputStream1 30 | assert listener 31 | assert stream 32 | assert inputStream 33 | } 34 | 35 | @Override 36 | void run() { 37 | try { 38 | log.trace("Started receiver $this") 39 | try { 40 | readStream() 41 | } catch (InterruptedIOException e) { 42 | log.debug("Interrupted receiver $this", e) 43 | } finally { 44 | inputStream.close() 45 | } 46 | } finally { 47 | log.trace("Finished receiver $this") 48 | } 49 | } 50 | 51 | private void readStream() { 52 | listener.start(stream) 53 | 54 | def readBuffer = new byte[READ_BUFFER_SIZE] 55 | while (!Thread.currentThread().interrupted) { 56 | log.trace("Waiting for $stream") 57 | def readLength = inputStream.read(readBuffer) 58 | if (readLength < 0) { 59 | log.trace("Reached end of stream on $stream") 60 | break 61 | } 62 | 63 | log.trace("Received $readLength bytes from $stream") 64 | pipes*.write(readBuffer, 0, readLength) 65 | listener.receive(stream, readBuffer, readLength) 66 | } 67 | 68 | listener.end(stream) 69 | } 70 | 71 | @Override 72 | String toString() { 73 | "${Receiver.simpleName}-${id}[${stream}]" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/interaction/Rule.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.interaction 2 | 3 | import groovy.transform.EqualsAndHashCode 4 | 5 | /** 6 | * A rule of interaction with the stream. 7 | * 8 | * @author Hidetake Iwata 9 | */ 10 | @EqualsAndHashCode 11 | class Rule { 12 | 13 | final Map condition 14 | 15 | final StreamRule streamRule 16 | final BufferRule bufferRule 17 | 18 | final Closure action 19 | 20 | def Rule(Map condition1, Closure action1) { 21 | condition = condition1 22 | action = action1 23 | 24 | streamRule = StreamRule.Factory.create(condition.from) 25 | bufferRule = BufferRule.Factory.create(condition) 26 | } 27 | 28 | MatchResult match(Stream stream, Buffer buffer) { 29 | if (streamRule.matches(stream)) { 30 | def matchResult = bufferRule.match(buffer) 31 | if (matchResult != null) { 32 | new MatchResult(this, matchResult) 33 | } else { 34 | null 35 | } 36 | } else { 37 | null 38 | } 39 | } 40 | 41 | String toString() { 42 | "${Rule.simpleName}${condition}" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/interaction/Stream.java: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.interaction; 2 | 3 | /** 4 | * Stream type. 5 | * Implemented as Java native enum for Gradle 1.x compatibility. 6 | * 7 | * @author Hidetake Iwata 8 | */ 9 | public enum Stream { 10 | StandardOutput, 11 | StandardError 12 | } 13 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/interaction/StreamRule.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.interaction 2 | 3 | interface StreamRule { 4 | boolean matches(Stream stream) 5 | 6 | static final anyRule = [matches: { Stream s -> true }] as StreamRule 7 | static final standardOutputRule = [matches: { Stream s -> s == Stream.StandardOutput }] as StreamRule 8 | static final standardErrorRule = [matches: { Stream s -> s == Stream.StandardError }] as StreamRule 9 | 10 | static class Factory { 11 | static StreamRule create(fromParameter) { 12 | switch (fromParameter) { 13 | case null: return anyRule 14 | case Wildcard: return anyRule 15 | case Stream.StandardOutput: return standardOutputRule 16 | case Stream.StandardError: return standardErrorRule 17 | default: throw new IllegalArgumentException("parameter must be Wildcard or Stream: from=$fromParameter") 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/interaction/Wildcard.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.interaction 2 | 3 | /** 4 | * Represents a wildcard. 5 | * 6 | * @author Hidetake Iwata 7 | */ 8 | @Singleton 9 | class Wildcard { 10 | String toString() { 11 | '_' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/operation/CommandSettings.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.operation 2 | 3 | import groovy.transform.EqualsAndHashCode 4 | import org.hidetake.groovy.ssh.core.settings.LoggingMethod 5 | import org.hidetake.groovy.ssh.core.settings.SettingsHelper 6 | import org.hidetake.groovy.ssh.core.settings.ToStringProperties 7 | 8 | /** 9 | * Settings for {@link Command}. 10 | * 11 | * @author Hidetake Iwata 12 | */ 13 | trait CommandSettings { 14 | /** 15 | * Ignores the exit status of the command or shell. 16 | */ 17 | Boolean ignoreError 18 | 19 | /** 20 | * PTY allocation flag. 21 | * If true, PTY will be allocated on command execution. 22 | */ 23 | Boolean pty 24 | 25 | /** 26 | * Use agentForwarding flag. 27 | * If true, agent will be forwarded to remote host. 28 | */ 29 | Boolean agentForwarding 30 | 31 | /** 32 | * A logging method of the remote command or shell. 33 | */ 34 | LoggingMethod logging 35 | 36 | /** 37 | * An input stream to send to the standard input. 38 | * This should be a {@link InputStream}, {@code byte[]}, {@link String} or {@link File}. 39 | */ 40 | def inputStream 41 | 42 | /** 43 | * An output stream to receive from the standard output. 44 | */ 45 | OutputStream outputStream 46 | 47 | /** 48 | * An output stream to receive from the standard error. 49 | */ 50 | OutputStream errorStream 51 | 52 | /** 53 | * Encoding of input and output stream. 54 | */ 55 | String encoding 56 | 57 | /** 58 | * Stream interaction. 59 | * @see org.hidetake.groovy.ssh.interaction.InteractionHandler 60 | */ 61 | Closure interaction 62 | 63 | /** 64 | * Timeout for the command channel to be connected in seconds. 65 | * @see org.hidetake.groovy.ssh.connection.ConnectionSettings#timeoutSec 66 | */ 67 | Integer timeoutSec 68 | 69 | 70 | @EqualsAndHashCode 71 | static class With implements CommandSettings, ToStringProperties { 72 | def With() {} 73 | def With(CommandSettings... sources) { 74 | SettingsHelper.mergeProperties(this, sources) 75 | } 76 | 77 | static final CommandSettings DEFAULT = new CommandSettings.With( 78 | ignoreError: false, 79 | pty: false, 80 | agentForwarding: false, 81 | logging: LoggingMethod.slf4j, 82 | encoding: 'UTF-8', 83 | timeoutSec: 0, 84 | ) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/operation/DefaultOperations.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.operation 2 | 3 | import groovy.util.logging.Slf4j 4 | import org.hidetake.groovy.ssh.connection.Connection 5 | import org.hidetake.groovy.ssh.core.Remote 6 | import org.hidetake.groovy.ssh.session.forwarding.LocalPortForwardSettings 7 | import org.hidetake.groovy.ssh.session.forwarding.RemotePortForwardSettings 8 | import org.hidetake.groovy.ssh.session.transfer.FileTransferSettings 9 | 10 | import static org.hidetake.groovy.ssh.util.Utility.callWithDelegate 11 | 12 | /** 13 | * Default implementation of {@link Operations}. 14 | * 15 | * Operations should follow the logging convention, that is, 16 | * it should write a log as DEBUG on beginning of an operation, 17 | * it should write a log as INFO on success of an operation, 18 | * but it does not need to write a log if it is an internal operation. 19 | 20 | * @author Hidetake Iwata 21 | */ 22 | @Slf4j 23 | class DefaultOperations implements Operations { 24 | final Remote remote 25 | 26 | private final Connection connection 27 | 28 | def DefaultOperations(Connection connection1) { 29 | connection = connection1 30 | remote = connection.remote 31 | assert connection 32 | assert remote 33 | } 34 | 35 | @Override 36 | Operation shell(ShellSettings settings) { 37 | log.debug("Executing shell on $remote: $settings") 38 | new Shell(connection, settings) 39 | } 40 | 41 | @Override 42 | Operation command(CommandSettings settings, String commandLine) { 43 | log.debug("Executing command on $remote: $commandLine: $settings") 44 | new Command(connection, settings, commandLine) 45 | } 46 | 47 | @Override 48 | int forwardLocalPort(LocalPortForwardSettings settings) { 49 | log.debug("Requesting local port forwarding on $remote with ${new LocalPortForwardSettings.With(settings)}") 50 | int port = connection.forwardLocalPort(settings) 51 | log.info("Enabled local port forwarding on $remote with ${new LocalPortForwardSettings.With(settings)}") 52 | port 53 | } 54 | 55 | @Override 56 | void forwardRemotePort(RemotePortForwardSettings settings) { 57 | log.debug("Requesting remote port forwarding on $remote with ${new RemotePortForwardSettings.With(settings)}") 58 | connection.forwardRemotePort(settings) 59 | log.info("Enabled remote port forwarding from on $remote with ${new RemotePortForwardSettings.With(settings)}") 60 | } 61 | 62 | @Override 63 | def T sftp(FileTransferSettings settings, @DelegatesTo(SftpOperations) Closure closure) { 64 | log.debug("Requesting SFTP subsystem on $remote") 65 | def channel = connection.createSftpChannel() 66 | channel.connect(settings.timeoutSec * 1000) 67 | try { 68 | log.debug("Started SFTP $remote.name#$channel.id") 69 | def result = callWithDelegate(closure, new SftpOperations(remote, channel)) 70 | log.debug("Finished SFTP $remote.name#$channel.id") 71 | result 72 | } finally { 73 | channel.disconnect() 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/operation/DryRunOperation.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.operation 2 | 3 | import org.hidetake.groovy.ssh.interaction.InteractionHandler 4 | 5 | class DryRunOperation implements Operation { 6 | @Override 7 | int execute() { 8 | 0 9 | } 10 | 11 | @Override 12 | void addInteraction(@DelegatesTo(InteractionHandler) Closure closure) { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/operation/DryRunOperations.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.operation 2 | 3 | import groovy.util.logging.Slf4j 4 | import org.hidetake.groovy.ssh.core.Remote 5 | import org.hidetake.groovy.ssh.session.forwarding.LocalPortForwardSettings 6 | import org.hidetake.groovy.ssh.session.forwarding.RemotePortForwardSettings 7 | import org.hidetake.groovy.ssh.session.transfer.FileTransferSettings 8 | 9 | /** 10 | * Dry-run implementation of {@link Operations}. 11 | * 12 | * @author Hidetake Iwata 13 | */ 14 | @Slf4j 15 | class DryRunOperations implements Operations { 16 | final Remote remote 17 | 18 | def DryRunOperations(Remote remote1) { 19 | remote = remote1 20 | assert remote 21 | } 22 | 23 | @Override 24 | Operation shell(ShellSettings settings) { 25 | log.info("Executing shell on $remote") 26 | new DryRunOperation() 27 | } 28 | 29 | @Override 30 | Operation command(CommandSettings settings, String commandLine) { 31 | log.info("Executing command on $remote: $commandLine") 32 | new DryRunOperation() 33 | } 34 | 35 | @Override 36 | int forwardLocalPort(LocalPortForwardSettings settings) { 37 | log.info("Requesting local port forwarding on $remote with ${new LocalPortForwardSettings.With(settings)}") 38 | 0 39 | } 40 | 41 | @Override 42 | void forwardRemotePort(RemotePortForwardSettings settings) { 43 | log.info("Requesting remote port forwarding on $remote with ${new RemotePortForwardSettings.With(settings)}") 44 | } 45 | 46 | @Override 47 | def T sftp(FileTransferSettings settings, @DelegatesTo(SftpOperations) Closure closure) { 48 | log.info("Requesting SFTP subsystem on $remote") 49 | null 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/operation/Operation.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.operation 2 | 3 | import org.hidetake.groovy.ssh.interaction.InteractionHandler 4 | 5 | /** 6 | * An operation such as a command or shell execution. 7 | * 8 | * @author Hidetake Iwata 9 | */ 10 | interface Operation { 11 | /** 12 | * Execute the operation. 13 | * 14 | * @return exit status 15 | */ 16 | int execute() 17 | 18 | /** 19 | * Adds an interaction. 20 | * 21 | * @param closure definition of interaction 22 | */ 23 | void addInteraction(@DelegatesTo(InteractionHandler) Closure closure) 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/operation/Operations.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.operation 2 | 3 | import org.hidetake.groovy.ssh.core.Remote 4 | import org.hidetake.groovy.ssh.session.forwarding.LocalPortForwardSettings 5 | import org.hidetake.groovy.ssh.session.forwarding.RemotePortForwardSettings 6 | import org.hidetake.groovy.ssh.session.transfer.FileTransferSettings 7 | 8 | /** 9 | * An aggregate of core SSH operations. 10 | * 11 | * @author Hidetake Iwata 12 | */ 13 | interface Operations { 14 | Remote getRemote() 15 | 16 | Operation shell(ShellSettings settings) 17 | 18 | Operation command(CommandSettings settings, String commandLine) 19 | 20 | int forwardLocalPort(LocalPortForwardSettings settings) 21 | 22 | void forwardRemotePort(RemotePortForwardSettings settings) 23 | 24 | def T sftp(FileTransferSettings settings, @DelegatesTo(SftpOperations) Closure closure) 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/operation/SftpException.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.operation 2 | 3 | import com.jcraft.jsch.SftpException as JschSftpException 4 | 5 | /** 6 | * Represents SFTP error. 7 | * 8 | * @author Hidetake Iwata 9 | */ 10 | class SftpException extends IOException { 11 | final SftpError error 12 | 13 | def SftpException(String contextMessage, JschSftpException cause) { 14 | this(contextMessage, cause, SftpError.find(cause.id)) 15 | } 16 | 17 | def SftpException(String contextMessage, JschSftpException cause, SftpError error) { 18 | super("$contextMessage: (${error.name()}: ${error.message}): ${cause.message}", cause) 19 | this.error = error 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/operation/SftpProgress.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.operation 2 | 3 | import com.jcraft.jsch.SftpProgressMonitor 4 | import groovy.transform.CompileStatic 5 | import org.hidetake.groovy.ssh.util.FileTransferProgress 6 | 7 | /** 8 | * A bridge class between {@link SftpProgressMonitor} and {@link FileTransferProgress}. 9 | * 10 | * Use {@link CompileStatic} to prevent {@link IncompatibleClassChangeError} on Gradle 1.x. 11 | * 12 | * @author Hidetake Iwata 13 | */ 14 | @CompileStatic 15 | class SftpProgress extends FileTransferProgress implements SftpProgressMonitor { 16 | 17 | def SftpProgress(Closure notifier) { 18 | super(notifier) 19 | } 20 | 21 | @Override 22 | void init(int op, String src, String dest, long max) { 23 | reset(max) 24 | } 25 | 26 | @Override 27 | boolean count(long count) { 28 | report(count) 29 | true 30 | } 31 | 32 | @Override 33 | void end() { 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/operation/ShellSettings.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.operation 2 | 3 | import groovy.transform.EqualsAndHashCode 4 | import org.hidetake.groovy.ssh.core.settings.LoggingMethod 5 | import org.hidetake.groovy.ssh.core.settings.SettingsHelper 6 | import org.hidetake.groovy.ssh.core.settings.ToStringProperties 7 | 8 | /** 9 | * Settings for {@link Shell}. 10 | * 11 | * @author Hidetake Iwata 12 | */ 13 | trait ShellSettings { 14 | /** 15 | * Ignores the exit status of the command or shell. 16 | */ 17 | Boolean ignoreError 18 | 19 | /** 20 | * PTY allocation flag. 21 | * If true, PTY will be allocated on command execution. 22 | */ 23 | Boolean pty 24 | 25 | /** 26 | * Use agentForwarding flag. 27 | * If true, agent will be forwarded to remote host. 28 | */ 29 | Boolean agentForwarding 30 | 31 | /** 32 | * A logging method of the remote command or shell. 33 | */ 34 | LoggingMethod logging 35 | 36 | /** 37 | * An input stream to send to the standard input. 38 | * This should be a {@link InputStream}, {@code byte[]}, {@link String} or {@link File}. 39 | */ 40 | def inputStream 41 | 42 | /** 43 | * An output stream to receive from the standard output. 44 | */ 45 | OutputStream outputStream 46 | 47 | /** 48 | * Encoding of input and output stream. 49 | */ 50 | String encoding 51 | 52 | /** 53 | * Stream interaction. 54 | * @see org.hidetake.groovy.ssh.interaction.InteractionHandler 55 | */ 56 | Closure interaction 57 | 58 | /** 59 | * Timeout for the shell channel to be connected in seconds. 60 | * @see org.hidetake.groovy.ssh.connection.ConnectionSettings#timeoutSec 61 | */ 62 | Integer timeoutSec 63 | 64 | 65 | @EqualsAndHashCode 66 | static class With implements ShellSettings, ToStringProperties { 67 | def With() {} 68 | def With(ShellSettings... sources) { 69 | SettingsHelper.mergeProperties(this, sources) 70 | } 71 | 72 | static final ShellSettings DEFAULT = new ShellSettings.With( 73 | ignoreError: false, 74 | pty: false, 75 | agentForwarding: false, 76 | logging: LoggingMethod.slf4j, 77 | encoding: 'UTF-8', 78 | timeoutSec: 0, 79 | ) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/BadExitStatusException.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | /** 6 | * An exception class thrown if the remote command returns bad exit status. 7 | * 8 | * @author Hidetake Iwata 9 | */ 10 | @CompileStatic 11 | class BadExitStatusException extends RuntimeException { 12 | final int exitStatus 13 | 14 | def BadExitStatusException(String message, int exitStatus) { 15 | super(message) 16 | this.exitStatus = exitStatus 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/Session.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session 2 | 3 | import groovy.transform.EqualsAndHashCode 4 | import groovy.util.logging.Slf4j 5 | import org.hidetake.groovy.ssh.core.Remote 6 | 7 | /** 8 | * A session. 9 | * 10 | * @author Hidetake Iwata 11 | */ 12 | @Slf4j 13 | @EqualsAndHashCode 14 | class Session { 15 | 16 | final Remote remote 17 | final Closure closure 18 | 19 | def Session(Remote remote1, Closure closure1) { 20 | remote = remote1 21 | closure = closure1 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/SessionExtension.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session 2 | 3 | import org.hidetake.groovy.ssh.core.Remote 4 | import org.hidetake.groovy.ssh.core.settings.CompositeSettings 5 | import org.hidetake.groovy.ssh.operation.Operations 6 | import org.hidetake.groovy.ssh.operation.SftpOperations 7 | 8 | /** 9 | * A base trait of session extensions. 10 | * Session extensions must apply this trait. 11 | * 12 | * @author Hidetake Iwata 13 | */ 14 | trait SessionExtension { 15 | /** 16 | * Returns remote host for the current session. 17 | * 18 | * @return the remote host 19 | */ 20 | abstract Remote getRemote() 21 | 22 | /** 23 | * Perform SFTP operations. 24 | * 25 | * @param closure closure for {@link org.hidetake.groovy.ssh.operation.SftpOperations} 26 | * @return result of the closure 27 | */ 28 | abstract def T sftp(@DelegatesTo(SftpOperations) Closure closure) 29 | 30 | /** 31 | * Return the current {@link Operations}. 32 | * Only for DSL extensions, do not use from the script. 33 | */ 34 | abstract Operations getOperations() 35 | 36 | /** 37 | * Return the settings with default, global, per-service and per-remote. 38 | * Only for DSL extensions, do not use from the script. 39 | */ 40 | abstract CompositeSettings getMergedSettings() 41 | } 42 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/SessionExtensions.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session 2 | 3 | import org.hidetake.groovy.ssh.session.execution.* 4 | import org.hidetake.groovy.ssh.session.forwarding.PortForward 5 | import org.hidetake.groovy.ssh.session.transfer.FileGet 6 | import org.hidetake.groovy.ssh.session.transfer.FilePut 7 | import org.hidetake.groovy.ssh.session.transfer.SftpRemove 8 | 9 | import groovy.util.logging.Slf4j 10 | 11 | /** 12 | * A set of extensions to be shipped as default. 13 | * 14 | * @author Hidetake Iwata 15 | */ 16 | @Slf4j 17 | trait SessionExtensions implements Command, BackgroundCommand, Script, 18 | Shell, Sudo, FileGet, FilePut, SftpRemove, PortForward { 19 | 20 | } -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/SessionHandler.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session 2 | 3 | import org.hidetake.groovy.ssh.core.Remote 4 | import org.hidetake.groovy.ssh.core.settings.CompositeSettings 5 | import org.hidetake.groovy.ssh.core.settings.GlobalSettings 6 | import org.hidetake.groovy.ssh.core.settings.PerServiceSettings 7 | import org.hidetake.groovy.ssh.operation.Operations 8 | import org.hidetake.groovy.ssh.operation.SftpOperations 9 | 10 | import groovy.util.logging.Slf4j 11 | 12 | /** 13 | * A handler of {@link org.hidetake.groovy.ssh.core.RunHandler#session(Remote, groovy.lang.Closure)}. 14 | * 15 | * @author Hidetake Iwata 16 | */ 17 | @Slf4j 18 | class SessionHandler implements SessionExtensions { 19 | final Operations operations 20 | 21 | /** 22 | * Settings with default, global, per-service and per-remote. 23 | */ 24 | final CompositeSettings mergedSettings 25 | 26 | static def create(Operations operations, GlobalSettings globalSettings, PerServiceSettings perServiceSettings) { 27 | def handler = new SessionHandler(operations, globalSettings, perServiceSettings) 28 | handler.mergedSettings.extensions.inject(handler) { applied, extension -> 29 | if (extension instanceof Class) { 30 | log.debug("Applying extension: $extension") 31 | applied.withTraits(extension) 32 | } else if (extension instanceof Map) { 33 | extension.each { String name, Closure implementation -> 34 | log.debug("Applying extension method: $name") 35 | applied.metaClass[name] = implementation 36 | } 37 | applied 38 | } else { 39 | log.error("Ignored unknown extension: $extension") 40 | applied 41 | } 42 | } 43 | } 44 | 45 | private def SessionHandler(Operations operations1, GlobalSettings globalSettings, PerServiceSettings perServiceSettings) { 46 | operations = operations1 47 | mergedSettings = new CompositeSettings.With( 48 | CompositeSettings.With.DEFAULT, 49 | globalSettings, 50 | perServiceSettings, 51 | operations.remote) 52 | } 53 | 54 | @Override 55 | Remote getRemote() { 56 | operations.remote 57 | } 58 | 59 | @Override 60 | def T sftp(@DelegatesTo(SftpOperations) Closure closure) { 61 | assert closure, 'closure must be given' 62 | operations.sftp(mergedSettings, closure) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/SessionSettings.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session 2 | 3 | import groovy.transform.EqualsAndHashCode 4 | import org.hidetake.groovy.ssh.core.settings.SettingsHelper 5 | import org.hidetake.groovy.ssh.core.settings.ToStringProperties 6 | 7 | /** 8 | * Settings for {@link org.hidetake.groovy.ssh.session.SessionHandler}s. 9 | * 10 | * @author Hidetake Iwata 11 | */ 12 | trait SessionSettings { 13 | /** 14 | * Dry-run flag. 15 | */ 16 | Boolean dryRun 17 | 18 | /** 19 | * JSch logging flag. 20 | */ 21 | Boolean jschLog 22 | 23 | /** 24 | * Extensions for {@link org.hidetake.groovy.ssh.session.SessionHandler}. 25 | */ 26 | List extensions = [] 27 | 28 | /** 29 | * Do not show if it is empty or null. 30 | */ 31 | def toString__extensions() { extensions ? extensions : null } 32 | 33 | def plus__extensions(SessionSettings prior) { 34 | assert prior.extensions instanceof List 35 | extensions + prior.extensions 36 | } 37 | 38 | 39 | @EqualsAndHashCode 40 | static class With implements SessionSettings, ToStringProperties { 41 | def With() {} 42 | def With(SessionSettings... sources) { 43 | SettingsHelper.mergeProperties(this, sources) 44 | } 45 | 46 | static final SessionSettings DEFAULT = new SessionSettings.With( 47 | dryRun: false, 48 | jschLog: false, 49 | extensions: [], 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/SessionTask.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session 2 | 3 | import groovy.transform.EqualsAndHashCode 4 | import groovy.util.logging.Slf4j 5 | import org.hidetake.groovy.ssh.connection.ConnectionManager 6 | import org.hidetake.groovy.ssh.connection.JSchLogger 7 | import org.hidetake.groovy.ssh.core.settings.CompositeSettings 8 | import org.hidetake.groovy.ssh.core.settings.GlobalSettings 9 | import org.hidetake.groovy.ssh.core.settings.PerServiceSettings 10 | import org.hidetake.groovy.ssh.operation.DefaultOperations 11 | import org.hidetake.groovy.ssh.operation.DryRunOperations 12 | 13 | import java.util.concurrent.Callable 14 | 15 | import static org.hidetake.groovy.ssh.util.Utility.callWithDelegate 16 | 17 | /** 18 | * A session with global and per-service settings. 19 | * 20 | * @author Hidetake Iwata 21 | */ 22 | @Slf4j 23 | @EqualsAndHashCode 24 | class SessionTask implements Callable { 25 | 26 | final Session session 27 | final GlobalSettings globalSettings 28 | final PerServiceSettings perServiceSettings 29 | 30 | def SessionTask(Session session1, GlobalSettings globalSettings1, PerServiceSettings perServiceSettings1) { 31 | session = session1 32 | globalSettings = globalSettings1 33 | perServiceSettings = perServiceSettings1 34 | } 35 | 36 | @Override 37 | def T call() { 38 | log.debug("Using per-remote settings: ${new CompositeSettings.With(session.remote)}") 39 | def settings = new SessionSettings.With( 40 | CompositeSettings.With.DEFAULT, 41 | globalSettings, 42 | perServiceSettings, 43 | session.remote) 44 | JSchLogger.enabledInCurrentThread = settings.jschLog 45 | if (settings.dryRun) { 46 | dryRun() 47 | } else { 48 | wetRun() 49 | } 50 | } 51 | 52 | private T dryRun() { 53 | def operations = new DryRunOperations(session.remote) 54 | def sessionHandler = SessionHandler.create(operations, globalSettings, perServiceSettings) 55 | callWithDelegate(session.closure, sessionHandler) 56 | } 57 | 58 | private T wetRun() { 59 | def manager = new ConnectionManager(globalSettings, perServiceSettings) 60 | try { 61 | def connection = manager.connect(session.remote) 62 | def operations = new DefaultOperations(connection) 63 | def sessionHandler = SessionHandler.create(operations, globalSettings, perServiceSettings) 64 | callWithDelegate(session.closure, sessionHandler) 65 | } finally { 66 | manager.close() 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/execution/BackgroundCommand.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.execution 2 | 3 | import groovy.util.logging.Slf4j 4 | 5 | /** 6 | * Provides the non-blocking command execution. 7 | * Each method returns immediately and executes the commandLine concurrently. 8 | * 9 | * @author Hidetake Iwata 10 | */ 11 | @Slf4j 12 | trait BackgroundCommand implements Command { 13 | @Deprecated 14 | void executeBackground(HashMap map = [:], String commandLine) { 15 | log.warn("Deprecated: executeBackground is no longer supported. Use execute instead.") 16 | execute(map, commandLine) 17 | } 18 | 19 | @Deprecated 20 | void executeBackground(HashMap map = [:], List commandLineArgs) { 21 | log.warn("Deprecated: executeBackground is no longer supported. Use execute instead.") 22 | execute(map, commandLineArgs) 23 | } 24 | 25 | @Deprecated 26 | void executeBackground(HashMap map = [:], String commandLine, Closure callback) { 27 | log.warn("Deprecated: executeBackground is no longer supported. Use execute instead.") 28 | execute(map, commandLine, callback) 29 | } 30 | 31 | @Deprecated 32 | void executeBackground(HashMap map = [:], List commandLineArgs, Closure callback) { 33 | log.warn("Deprecated: executeBackground is no longer supported. Use execute instead.") 34 | execute(map, commandLineArgs, callback) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/execution/Command.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.execution 2 | 3 | import org.codehaus.groovy.tools.Utilities 4 | import org.hidetake.groovy.ssh.operation.CommandSettings 5 | import org.hidetake.groovy.ssh.operation.Operations 6 | import org.hidetake.groovy.ssh.session.BadExitStatusException 7 | import org.hidetake.groovy.ssh.session.SessionExtension 8 | 9 | import groovy.util.logging.Slf4j 10 | 11 | /** 12 | * Provides the blocking command execution. 13 | * Each method blocks until channel is closed. 14 | * 15 | * @author Hidetake Iwata 16 | */ 17 | @Slf4j 18 | trait Command implements SessionExtension { 19 | void execute(HashMap map = [:], String commandLine, Closure callback) { 20 | assert callback, 'callback must be given' 21 | callback.call(execute(map, commandLine)) 22 | } 23 | 24 | void execute(HashMap map = [:], List commandLineArgs, Closure callback) { 25 | assert callback, 'callback must be given' 26 | callback.call(execute(map, commandLineArgs)) 27 | } 28 | 29 | String execute(HashMap map = [:], String commandLine) { 30 | assert commandLine, 'commandLine must be given' 31 | assert map != null, 'map must not be null' 32 | def settings = new CommandSettings.With(mergedSettings, new CommandSettings.With(map)) 33 | Helper.execute(operations, settings, commandLine) 34 | } 35 | 36 | String execute(HashMap map = [:], List commandLineArgs) { 37 | assert commandLineArgs, 'commandLineArgs must be given' 38 | assert map != null, 'map must not be null' 39 | execute(map, Escape.escape(commandLineArgs)) 40 | } 41 | 42 | static class Helper { 43 | static execute(Operations operations, CommandSettings settings, String commandLine) { 44 | def operation = operations.command(settings, commandLine) 45 | 46 | def lines = [] as List 47 | operation.addInteraction { 48 | when(line: _, from: standardOutput) { String line -> 49 | lines.add(line) 50 | } 51 | } 52 | 53 | def exitStatus = operation.execute() 54 | if (exitStatus != 0 && !settings.ignoreError) { 55 | throw new BadExitStatusException("Command returned exit status $exitStatus: $commandLine", exitStatus) 56 | } 57 | lines.join(Utilities.eol()) 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/execution/Escape.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.execution 2 | 3 | /** 4 | * Shell escape utility. 5 | * 6 | * @author Hidetake Iwata 7 | */ 8 | class Escape { 9 | 10 | /** 11 | * Escape command arguments. 12 | * This method quotes each argument with single-quote. 13 | * @param arguments 14 | * @return 15 | */ 16 | static String escape(List arguments) { 17 | arguments.collect { /'${it.replaceAll(~/'/, /'\\''/)}'/ }.join(/ /) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/execution/Script.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.execution 2 | 3 | import groovy.util.logging.Slf4j 4 | 5 | /** 6 | * An extension class for script execution. 7 | * 8 | * @author Hidetake Iwata 9 | */ 10 | @Slf4j 11 | trait Script implements Command { 12 | 13 | String executeScript(HashMap settings = [:], String script) { 14 | assert script, 'script must be given' 15 | execute(Helper.createSettings(settings, script), Helper.guessInterpreter(script)) 16 | } 17 | 18 | void executeScript(HashMap settings = [:], String script, Closure callback) { 19 | assert script, 'script must be given' 20 | execute(Helper.createSettings(settings, script), Helper.guessInterpreter(script), callback) 21 | } 22 | 23 | String executeScript(HashMap settings = [:], File script) { 24 | assert script, 'script must be given' 25 | execute(Helper.createSettings(settings, script), Helper.guessInterpreter(script)) 26 | } 27 | 28 | void executeScript(HashMap settings = [:], File script, Closure callback) { 29 | assert script, 'script must be given' 30 | execute(Helper.createSettings(settings, script), Helper.guessInterpreter(script), callback) 31 | } 32 | 33 | static class Helper { 34 | static HashMap createSettings(HashMap settings, def script) { 35 | if (settings.inputStream) { 36 | throw new IllegalArgumentException("executeScript does not work with inputStream: $settings") 37 | } 38 | [:] << settings << [inputStream: script] as HashMap 39 | } 40 | 41 | static String guessInterpreter(String script) { 42 | script.find(~/^#!.+/) { m -> m.substring(2) } ?: '/bin/sh' 43 | } 44 | 45 | static String guessInterpreter(File script) { 46 | script.withReader { reader -> 47 | reader.readLine().find(~/^#!.+/) { m -> m.substring(2) } 48 | } ?: '/bin/sh' 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/execution/Shell.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.execution 2 | 3 | import org.hidetake.groovy.ssh.operation.ShellSettings 4 | import org.hidetake.groovy.ssh.session.BadExitStatusException 5 | import org.hidetake.groovy.ssh.session.SessionExtension 6 | 7 | /** 8 | * Provides the shell execution. 9 | * 10 | * @author Hidetake Iwata 11 | */ 12 | trait Shell implements SessionExtension { 13 | /** 14 | * Performs a shell operation. 15 | * This method blocks until channel is closed. 16 | * 17 | * @param map shell settings 18 | * @return output value of the command 19 | */ 20 | void shell(HashMap map) { 21 | assert map != null, 'map must not be null' 22 | 23 | def settings = new ShellSettings.With(mergedSettings, new ShellSettings.With(map)) 24 | def operation = operations.shell(settings) 25 | 26 | def exitStatus = operation.execute() 27 | if (exitStatus != 0 && !settings.ignoreError) { 28 | throw new BadExitStatusException("Shell returned exit status $exitStatus", exitStatus) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/execution/Sudo.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.execution 2 | 3 | import org.hidetake.groovy.ssh.session.SessionExtension 4 | 5 | /** 6 | * An extension class of sudo command execution. 7 | * Each method performs a sudo operation, explicitly providing password for the sudo user. 8 | * It blocks until channel is closed. 9 | * 10 | * @author Hidetake Iwata 11 | */ 12 | trait Sudo implements SessionExtension { 13 | 14 | String executeSudo(HashMap settings = [:], String commandLine) { 15 | assert commandLine, 'commandLine must be given' 16 | assert settings != null, 'settings must not be null' 17 | def helper = new SudoHelper(operations, mergedSettings, new SudoHelper.SudoCommandSettings(settings)) 18 | helper.execute(commandLine) 19 | } 20 | 21 | String executeSudo(HashMap settings = [:], List commandLineArgs) { 22 | executeSudo(settings, Escape.escape(commandLineArgs)) 23 | } 24 | 25 | void executeSudo(HashMap settings = [:], String commandLine, Closure callback) { 26 | assert commandLine, 'commandLine must be given' 27 | assert callback, 'callback must be given' 28 | assert settings != null, 'settings must not be null' 29 | callback.call(executeSudo(settings, commandLine)) 30 | } 31 | 32 | void executeSudo(HashMap settings = [:], List commandLineArgs, Closure callback) { 33 | executeSudo(settings, Escape.escape(commandLineArgs), callback) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/execution/SudoException.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.execution 2 | 3 | import groovy.transform.CompileStatic 4 | import org.hidetake.groovy.ssh.core.Remote 5 | 6 | /** 7 | * An exception class thrown if sudo authentication failed. 8 | * 9 | * @author Hidetake Iwata 10 | */ 11 | @CompileStatic 12 | class SudoException extends RuntimeException { 13 | 14 | final Remote remote 15 | 16 | def SudoException(Remote remote, String message) { 17 | super("Failed sudo authentication on $remote: $message") 18 | this.remote = remote 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/execution/SudoSettings.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.execution 2 | 3 | import groovy.transform.EqualsAndHashCode 4 | import org.hidetake.groovy.ssh.core.settings.SettingsHelper 5 | import org.hidetake.groovy.ssh.core.settings.ToStringProperties 6 | 7 | trait SudoSettings { 8 | /** 9 | * Sudo password. 10 | */ 11 | String sudoPassword 12 | 13 | /** 14 | * Sudo executable path. 15 | */ 16 | String sudoPath 17 | 18 | /** 19 | * An input stream to send to the standard input. 20 | * @see org.hidetake.groovy.ssh.operation.CommandSettings#inputStream 21 | */ 22 | def inputStream 23 | 24 | 25 | @EqualsAndHashCode 26 | static class With implements SudoSettings, ToStringProperties { 27 | def With() {} 28 | def With(SudoSettings... sources) { 29 | SettingsHelper.mergeProperties(this, sources) 30 | } 31 | 32 | static final SudoSettings DEFAULT = new SudoSettings.With( 33 | sudoPassword: null, 34 | sudoPath: 'sudo', 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/forwarding/LocalPortForwardSettings.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.forwarding 2 | 3 | import groovy.transform.EqualsAndHashCode 4 | import org.hidetake.groovy.ssh.core.settings.SettingsHelper 5 | import org.hidetake.groovy.ssh.core.settings.ToStringProperties 6 | 7 | /** 8 | * Settings for the local port forwarding. 9 | * 10 | * @author Hidetake Iwata 11 | */ 12 | trait LocalPortForwardSettings { 13 | /** 14 | * Local port to bind. Defaults to 0 (allocate free port). 15 | */ 16 | Integer port 17 | 18 | /** 19 | * Local host to bind. Defaults to localhost. 20 | */ 21 | String bind 22 | 23 | /** 24 | * Remote port to connect. (Mandatory) 25 | */ 26 | Integer hostPort 27 | 28 | /** 29 | * Remote host to connect. Default to localhost of the remote host. 30 | */ 31 | String host 32 | 33 | 34 | @EqualsAndHashCode 35 | static class With implements LocalPortForwardSettings, ToStringProperties { 36 | def With() {} 37 | def With(LocalPortForwardSettings... sources) { 38 | SettingsHelper.mergeProperties(this, sources) 39 | } 40 | 41 | static final DEFAULT = new With(port: 0, bind: '127.0.0.1', host: '127.0.0.1') 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/forwarding/PortForward.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.forwarding 2 | 3 | import org.hidetake.groovy.ssh.session.SessionExtension 4 | 5 | /** 6 | * An extension class of port forwarding. 7 | * 8 | * @author Hidetake Iwata 9 | */ 10 | trait PortForward implements SessionExtension { 11 | 12 | /** 13 | * Forwards local port to remote port. 14 | * 15 | * @param settings {@see LocalPortForwardingSettings} 16 | * @return local port 17 | */ 18 | int forwardLocalPort(HashMap settings) { 19 | assert settings != null, 'settings must not be null' 20 | def merged = new LocalPortForwardSettings.With(LocalPortForwardSettings.With.DEFAULT, new LocalPortForwardSettings.With(settings)) 21 | assert merged.hostPort, 'remote port must be given' 22 | operations.forwardLocalPort(merged) 23 | } 24 | 25 | /** 26 | * Forwards remote port to local port. 27 | * 28 | * @param settings {@see RemotePortForwardingSettings} 29 | */ 30 | void forwardRemotePort(HashMap settings) { 31 | assert settings != null, 'settings must not be null' 32 | def merged = new RemotePortForwardSettings.With(RemotePortForwardSettings.With.DEFAULT, new RemotePortForwardSettings.With(settings)) 33 | assert merged.hostPort, 'local port must be given' 34 | assert merged.port, 'remote port must be given' 35 | operations.forwardRemotePort(merged) 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/forwarding/RemotePortForwardSettings.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.forwarding 2 | 3 | import groovy.transform.EqualsAndHashCode 4 | import org.hidetake.groovy.ssh.core.settings.SettingsHelper 5 | import org.hidetake.groovy.ssh.core.settings.ToStringProperties 6 | 7 | /** 8 | * Settings for the remote port forwarding. 9 | * 10 | * @author Hidetake Iwata 11 | */ 12 | trait RemotePortForwardSettings { 13 | /** 14 | * Local port to connect. (Mandatory) 15 | */ 16 | Integer hostPort 17 | 18 | /** 19 | * Local host to connect. Defaults to localhost. 20 | */ 21 | String host 22 | 23 | /** 24 | * Remote port to bind. (Mandatory) 25 | */ 26 | Integer port 27 | 28 | /** 29 | * Remote host to bind. Default to localhost of the remote host. 30 | */ 31 | String bind 32 | 33 | 34 | @EqualsAndHashCode 35 | static class With implements RemotePortForwardSettings, ToStringProperties { 36 | def With() {} 37 | def With(RemotePortForwardSettings... sources) { 38 | SettingsHelper.mergeProperties(this, sources) 39 | } 40 | 41 | static final DEFAULT = new With(bind: '127.0.0.1', host: '127.0.0.1') 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/transfer/FileTransferMethod.java: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.transfer; 2 | 3 | /** 4 | * File transfer method type. 5 | * Implemented as Java native enum for Gradle 1.x compatibility. 6 | * 7 | * @author Hidetake Iwata 8 | */ 9 | public enum FileTransferMethod { 10 | /** 11 | * Transfer via SFTP channel 12 | */ 13 | sftp, 14 | 15 | /** 16 | * Transfer via SCP command 17 | */ 18 | scp 19 | } 20 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/transfer/FileTransferSettings.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.transfer 2 | 3 | import groovy.transform.EqualsAndHashCode 4 | import org.hidetake.groovy.ssh.core.settings.SettingsHelper 5 | 6 | trait FileTransferSettings { 7 | 8 | /** 9 | * File transfer method such as SFTP or SCP. 10 | */ 11 | FileTransferMethod fileTransfer 12 | 13 | /** 14 | * Timeout for the SFTP or command channel to be connected in seconds. 15 | * @see org.hidetake.groovy.ssh.connection.ConnectionSettings#timeoutSec 16 | */ 17 | Integer timeoutSec 18 | 19 | 20 | @EqualsAndHashCode 21 | static class With implements FileTransferSettings { 22 | def With() {} 23 | def With(FileTransferSettings... sources) { 24 | SettingsHelper.mergeProperties(this, sources) 25 | } 26 | 27 | static FileTransferSettings DEFAULT = new FileTransferSettings.With( 28 | fileTransfer: FileTransferMethod.sftp, 29 | timeoutSec: 0, 30 | ) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/transfer/SftpRemove.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.transfer 2 | 3 | import groovy.util.logging.Slf4j 4 | import org.hidetake.groovy.ssh.operation.SftpError 5 | import org.hidetake.groovy.ssh.operation.SftpException 6 | import org.hidetake.groovy.ssh.session.SessionExtension 7 | 8 | import static org.hidetake.groovy.ssh.util.Utility.currySelf 9 | 10 | /** 11 | * An extension class to remove a file or directory via SFTP. 12 | * 13 | * @author Hidetake Iwata 14 | */ 15 | @Slf4j 16 | trait SftpRemove implements SessionExtension { 17 | 18 | /** 19 | * Remove files or directories. 20 | * This method silently ignores non-existing files or directories but 21 | * throws an exception if any error occurs. 22 | * 23 | * @param paths files or directories 24 | * @return true if anything got removed, false if nothing done 25 | */ 26 | boolean remove(String... paths) { 27 | sftp { 28 | paths.collect { remotePath -> 29 | def remoteAttrs = Helper.nullIfNoSuchFile { stat(remotePath) } 30 | if (remoteAttrs == null) { 31 | false 32 | } else if (remoteAttrs.dir) { 33 | log.debug("Entering directory on $remote.name: $remotePath") 34 | cd(remotePath) 35 | 36 | currySelf { Closure self -> 37 | def entries = ls('.') 38 | entries.findAll { !it.attrs.dir }.each { child -> 39 | log.debug("Removing file on $remote.name: $child.filename") 40 | rm(child.filename) 41 | } 42 | entries.findAll { it.attrs.dir && !(it.filename in ['.', '..']) }.each { child -> 43 | log.debug("Entering directory on $remote.name: $child.filename") 44 | cd(child.filename) 45 | self() 46 | log.debug("Leaving directory on $remote.name: $child.filename") 47 | cd('..') 48 | log.debug("Removing directory on $remote.name: $child.filename") 49 | rmdir(child.filename) 50 | } 51 | }() 52 | 53 | log.debug("Leaving directory on $remote.name: $remotePath") 54 | cd('..') 55 | log.debug("Removing directory on $remote.name: $remotePath") 56 | rmdir(remotePath) 57 | log.info("Removed directory on $remote.name: $remotePath") 58 | true 59 | } else { 60 | rm(remotePath) 61 | log.info("Removed file on $remote.name: $remotePath") 62 | true 63 | } 64 | }.any() 65 | } 66 | } 67 | 68 | private static class Helper { 69 | static T nullIfNoSuchFile(Closure closure) { 70 | try { 71 | closure() 72 | } catch (SftpException e) { 73 | if (e.error == SftpError.SSH_FX_NO_SUCH_FILE) { 74 | null 75 | } else { 76 | throw e 77 | } 78 | } 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/transfer/get/FileReceiver.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.transfer.get 2 | 3 | import groovy.util.logging.Slf4j 4 | 5 | @Slf4j 6 | class FileReceiver implements WritableReceiver { 7 | 8 | final File destination 9 | 10 | @Lazy 11 | private recreateAtFirst = { 12 | if (destination.exists()) { 13 | destination.delete() 14 | } 15 | destination.withWriter { writer -> writer.flush() } 16 | ({}) 17 | }() 18 | 19 | def FileReceiver(File destination1) { 20 | destination = destination1 21 | assert !destination.directory 22 | } 23 | 24 | @Override 25 | void write(byte[] bytes) { 26 | recreateAtFirst() 27 | log.trace("Writing $bytes.length bytes into file: $destination") 28 | destination.append(bytes) 29 | } 30 | 31 | @Override 32 | String toString() { 33 | destination.path 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/transfer/get/Provider.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.transfer.get 2 | /** 3 | * An interface of file GET provider. 4 | * 5 | * @author Hidetake Iwata 6 | */ 7 | interface Provider { 8 | 9 | void get(String remotePath, RecursiveReceiver receiver) 10 | 11 | void get(String remotePath, FileReceiver receiver) 12 | 13 | void get(String remotePath, StreamReceiver receiver) 14 | 15 | } -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/transfer/get/RecursiveReceiver.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.transfer.get 2 | 3 | import groovy.util.logging.Slf4j 4 | 5 | @Slf4j 6 | class RecursiveReceiver { 7 | 8 | final File destination 9 | 10 | private final Closure filter 11 | private final ArrayDeque directoryStack 12 | 13 | def RecursiveReceiver(File destination1, Closure filter1) { 14 | destination = destination1 15 | filter = filter1 16 | directoryStack = [destination] 17 | assert destination.directory 18 | } 19 | 20 | /** 21 | * Called when the remote file is found. 22 | * 23 | * @param name 24 | * @return file 25 | */ 26 | File createFile(String name) { 27 | def directory = directoryStack.getLast() 28 | def file = new File(directory, name) 29 | if (!filter || filter.call(file)) { 30 | if (!directory.exists()) { 31 | directory.mkdirs() 32 | } else if (file.exists()) { 33 | file.delete() 34 | } 35 | file.createNewFile() 36 | file 37 | } else { 38 | null 39 | } 40 | } 41 | 42 | /** 43 | * Called when it entered into the remote directory. 44 | * 45 | * @param name 46 | */ 47 | void enterDirectory(String name) { 48 | def directory = new File(directoryStack.getLast(), name) 49 | directoryStack.addLast(directory) 50 | if (!filter) { 51 | directory.mkdirs() 52 | } 53 | } 54 | 55 | /** 56 | * Called when it left from the remote directory. 57 | */ 58 | void leaveDirectory() { 59 | directoryStack.removeLast() 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/transfer/get/ScpException.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.transfer.get 2 | 3 | /** 4 | * Represents SCP error. 5 | * 6 | * @author Hidetake Iwata 7 | */ 8 | class ScpException extends IOException { 9 | def ScpException(String message) { 10 | super(message) 11 | } 12 | 13 | def ScpException(String message, Throwable cause) { 14 | super(message, cause) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/transfer/get/Sftp.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.transfer.get 2 | 3 | import groovy.util.logging.Slf4j 4 | import org.hidetake.groovy.ssh.operation.Operations 5 | import org.hidetake.groovy.ssh.session.transfer.FileTransferSettings 6 | 7 | import static org.hidetake.groovy.ssh.util.Utility.currySelf 8 | 9 | /** 10 | * A helper class for SFTP get operation. 11 | * 12 | * @author Hidetake Iwata 13 | */ 14 | @Slf4j 15 | class Sftp implements Provider { 16 | 17 | final Operations operations 18 | final FileTransferSettings mergedSettings 19 | 20 | def Sftp(Operations operations1, FileTransferSettings mergedSettings1) { 21 | operations = operations1 22 | mergedSettings = mergedSettings1 23 | } 24 | 25 | void get(String remotePath, FileReceiver receiver) { 26 | operations.sftp(mergedSettings) { 27 | getFile(remotePath, receiver.destination.path) 28 | log.info("Received file from $operations.remote.name: $remotePath -> $receiver.destination") 29 | } 30 | } 31 | 32 | void get(String remotePath, StreamReceiver receiver) { 33 | operations.sftp(mergedSettings) { 34 | getContent(remotePath, receiver.stream) 35 | log.info("Received content from $operations.remote.name: $remotePath") 36 | } 37 | } 38 | 39 | void get(String remotePath, RecursiveReceiver receiver) { 40 | operations.sftp(mergedSettings) { 41 | def remoteAttrs = stat(remotePath) 42 | if (remoteAttrs.dir) { 43 | log.debug("Entering directory on $remote.name: $remotePath") 44 | cd(remotePath) 45 | receiver.enterDirectory(remotePath.find(~'[^/]+/?$')) 46 | 47 | currySelf { Closure self -> 48 | def entries = ls('.') 49 | entries.findAll { !it.attrs.dir }.each { child -> 50 | def localFile = receiver.createFile(child.filename) 51 | if (localFile) { 52 | getFile(child.filename, localFile.path) 53 | log.info("Received file from $operations.remote.name: $child.filename -> $localFile") 54 | } 55 | } 56 | entries.findAll { it.attrs.dir && !(it.filename in ['.', '..']) }.each { child -> 57 | log.debug("Entering directory on $remote.name: $child.filename") 58 | cd(child.filename) 59 | receiver.enterDirectory(child.filename) 60 | self() 61 | log.debug("Leaving directory on $remote.name: $child.filename") 62 | cd('..') 63 | receiver.leaveDirectory() 64 | } 65 | }() 66 | 67 | log.info("Received directory from $operations.remote.name: $remotePath -> $receiver.destination") 68 | } else { 69 | getFile(remotePath, receiver.destination.path) 70 | log.info("Received file from $operations.remote.name: $remotePath -> $receiver.destination") 71 | } 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/transfer/get/StreamReceiver.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.transfer.get 2 | 3 | import groovy.util.logging.Slf4j 4 | 5 | @Slf4j 6 | class StreamReceiver implements WritableReceiver { 7 | 8 | final OutputStream stream 9 | 10 | def StreamReceiver(OutputStream stream1) { 11 | stream = stream1 12 | } 13 | 14 | @Override 15 | void write(byte[] bytes) { 16 | log.trace("Writing $bytes.length bytes into the stream") 17 | stream.write(bytes) 18 | } 19 | 20 | @Override 21 | String toString() { 22 | stream.class.simpleName 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/transfer/get/WritableReceiver.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.transfer.get 2 | 3 | interface WritableReceiver { 4 | 5 | void write(byte[] bytes) 6 | 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/transfer/put/EnterDirectory.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.transfer.put 2 | 3 | import groovy.transform.ToString 4 | 5 | @ToString 6 | class EnterDirectory { 7 | final String name 8 | 9 | def EnterDirectory(String name1) { 10 | name = name1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/transfer/put/LeaveDirectory.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.transfer.put 2 | 3 | import groovy.transform.ToString 4 | 5 | @Singleton 6 | @ToString 7 | class LeaveDirectory { 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/transfer/put/Provider.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.transfer.put 2 | 3 | /** 4 | * An interface of file PUT provider. 5 | * 6 | * @author Hidetake Iwata 7 | */ 8 | interface Provider { 9 | 10 | void put(Instructions instructions) 11 | 12 | } -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/transfer/put/Sftp.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.transfer.put 2 | 3 | import groovy.util.logging.Slf4j 4 | import org.hidetake.groovy.ssh.operation.Operations 5 | import org.hidetake.groovy.ssh.operation.SftpException 6 | import org.hidetake.groovy.ssh.session.transfer.FileTransferSettings 7 | 8 | import static org.hidetake.groovy.ssh.operation.SftpError.SSH_FX_FAILURE 9 | import static org.hidetake.groovy.ssh.operation.SftpError.SSH_FX_FILE_ALREADY_EXISTS 10 | 11 | /** 12 | * Recursive SFTP PUT executor. 13 | * 14 | * @author Hidetake Iwata 15 | */ 16 | @Slf4j 17 | class Sftp implements Provider { 18 | 19 | private final Operations operations 20 | final FileTransferSettings mergedSettings 21 | 22 | def Sftp(Operations operations1, FileTransferSettings mergedSettings1) { 23 | operations = operations1 24 | mergedSettings = mergedSettings1 25 | } 26 | 27 | void put(Instructions instructions) { 28 | def directoryStack = [instructions.base] as ArrayDeque 29 | operations.sftp(mergedSettings) { 30 | instructions.each { instruction -> 31 | def remotePath = directoryStack.join('/') 32 | 33 | log.trace("Processing instruction: $instruction") 34 | switch (instruction) { 35 | case File: 36 | def file = instruction as File 37 | putFile(file.path, remotePath) 38 | log.info("Sent file to $remote.name: $file -> $remotePath") 39 | break 40 | 41 | case StreamContent: 42 | def content = instruction as StreamContent 43 | def remoteFile = "$remotePath/$content.name" 44 | putContent(content.stream, remoteFile) 45 | log.info("Sent content to $remote.name: $remoteFile") 46 | break 47 | 48 | case EnterDirectory: 49 | def directory = instruction as EnterDirectory 50 | def remoteDir = "$remotePath/$directory.name" 51 | try { 52 | mkdir(remoteDir) 53 | } catch (SftpException e) { 54 | if (e.error in [SSH_FX_FILE_ALREADY_EXISTS, SSH_FX_FAILURE]) { 55 | log.info("Remote directory already exists on $remote.name: $remoteDir") 56 | } else { 57 | throw e 58 | } 59 | } 60 | directoryStack.addLast(directory.name) 61 | break 62 | 63 | case LeaveDirectory: 64 | directoryStack.removeLast() 65 | break 66 | 67 | default: 68 | throw new IllegalStateException("Unknown instruction type: $instruction") 69 | } 70 | } 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/session/transfer/put/StreamContent.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.transfer.put 2 | 3 | import groovy.transform.ToString 4 | 5 | @ToString 6 | class StreamContent { 7 | final String name 8 | final InputStream stream 9 | 10 | def StreamContent(String name1, InputStream stream1) { 11 | name = name1 12 | stream = stream1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/util/ManagedBlocking.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.util 2 | 3 | import java.util.concurrent.ForkJoinPool 4 | 5 | /** 6 | * A convenient class of {@link ForkJoinPool.ManagedBlocker}. 7 | * 8 | * @author Hidetake Iwata 9 | */ 10 | class ManagedBlocking { 11 | /** 12 | * Wait until condition is satisfied. 13 | * 14 | * @param intervalMillis polling interval 15 | * @param condition returns true if polling is completed 16 | */ 17 | static void until(long intervalMillis = 100L, Closure condition) { 18 | ForkJoinPool.managedBlock(new ForkJoinPool.ManagedBlocker() { 19 | @Override 20 | boolean block() throws InterruptedException { 21 | if (!isReleasable()) { 22 | sleep(intervalMillis) 23 | } 24 | isReleasable() 25 | } 26 | 27 | @Override 28 | boolean isReleasable() { 29 | condition.call() 30 | } 31 | }) 32 | } 33 | 34 | /** 35 | * Wait given time. 36 | * @param millis 37 | */ 38 | static void sleep(long millis) { 39 | def started = System.currentTimeMillis() 40 | until { 41 | (System.currentTimeMillis() - started) > millis 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /core/src/main/groovy/org/hidetake/groovy/ssh/util/Utility.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.util 2 | 3 | import com.jcraft.jsch.JSchException 4 | import groovy.util.logging.Slf4j 5 | 6 | /** 7 | * Provides utility methods. 8 | * 9 | * @author Hidetake Iwata 10 | */ 11 | @Slf4j 12 | class Utility { 13 | static T callWithDelegate(Closure closure, delegate, ... arguments) { 14 | def cloned = closure.clone() as Closure 15 | cloned.resolveStrategy = Closure.DELEGATE_FIRST 16 | cloned.delegate = delegate 17 | cloned.call(*arguments) 18 | } 19 | 20 | /** 21 | * Curry a method with self for recursive. 22 | * 23 | * @param closure 24 | * @return curried closure 25 | */ 26 | static Closure currySelf(Closure closure) { 27 | def curried 28 | curried = closure.curry { 29 | closure.call(curried) 30 | } 31 | } 32 | 33 | /** 34 | * Execute the closure with retrying. 35 | * This method catches only {@link com.jcraft.jsch.JSchException}s. 36 | * 37 | * @param retryCount 38 | * @param retryWaitSec 39 | * @param closure 40 | */ 41 | static T retry(int retryCount, int retryWaitSec, Closure closure) { 42 | assert closure != null, 'closure should be set' 43 | if (retryCount > 0) { 44 | try { 45 | closure() 46 | } catch (JSchException e) { 47 | log.warn("Retrying: ${e.getClass().name}: ${e.localizedMessage}") 48 | ManagedBlocking.sleep(retryWaitSec * 1000L) 49 | retry(retryCount - 1, retryWaitSec, closure) 50 | } 51 | } else { 52 | closure() 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/src/main/resources/org/hidetake/groovy/ssh/Release.properties: -------------------------------------------------------------------------------- 1 | product.name=groovy-ssh 2 | product.version=@version@ 3 | -------------------------------------------------------------------------------- /core/src/test/groovy/org/hidetake/groovy/ssh/SshClassSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh 2 | 3 | import spock.lang.Specification 4 | 5 | class SshClassSpec extends Specification { 6 | 7 | def "version property should be the product version"() { 8 | expect: 9 | Ssh.release.version.matches(/@version@|SNAPSHOT|[0-9\.]+/) 10 | } 11 | 12 | def "name property should be the product name"() { 13 | expect: 14 | !Ssh.release.name.empty 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /core/src/test/groovy/org/hidetake/groovy/ssh/connection/HostKeyRepositorySpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.connection 2 | 3 | import com.jcraft.jsch.HostKey 4 | import spock.lang.Specification 5 | import spock.lang.Unroll 6 | 7 | class HostKeyRepositorySpec extends Specification { 8 | 9 | @Unroll 10 | def "comparing of JSch HostKey and raw host and port"() { 11 | given: 12 | def hostKey = new HostKey(jhost, HostKey.SSHRSA ,null) 13 | 14 | expect: 15 | HostKeyRepository.compare( hostKey, host, port ) == expected 16 | 17 | where: 18 | jhost | host | port || expected 19 | 'simple' | 'simple' | 22 || true 20 | 'notsimple' | 'simple' | 22 || false 21 | '[simple]:4321' | 'simple' | 4321 || true 22 | 'simple,[notsimple]:4321' | 'simple' | 22 || true 23 | 'simple,[notsimple]:4321' | 'simple' | 4321 || false 24 | 'simple,[notsimple]:4321' | 'notsimple' | 4321 || true 25 | 'simple,[notsimple]:4321,more' | 'more' | 22 || true 26 | 'simple,[notsimple]:4321,more' | 'notsimple' | 4321 || true 27 | 28 | '|1|c2FsdA==|Ip5vCJkMOpZGFriXFS4Jiw1khnY=' | 'hashme' | 22 || true 29 | '|1|c2FsdA==|Ip5vCJkMOpZGFriXFS4Jiw1khnY=' | 'hashme' | 4321 || false 30 | '|1|c2FsdHk=|4/+CSFSbcjz5uEJcO5B77hM70RM=' | 'hashmeport' | 4321 || true 31 | '|1|c2FsdHk=|4/+CSFSbcjz5uEJcO5B77hM70RM=' | 'hashmeport' | 22 || false 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /core/src/test/groovy/org/hidetake/groovy/ssh/core/ProxySpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core 2 | 3 | import spock.lang.Specification 4 | 5 | import static org.hidetake.groovy.ssh.core.ProxyType.SOCKS 6 | 7 | class ProxySpec extends Specification { 8 | 9 | def "result of toString() does not contain password"() { 10 | given: 11 | def proxy = new Proxy('theProxy') 12 | proxy.host = 'theHost' 13 | proxy.user = 'theUser' 14 | proxy.password = 'thePassword' 15 | proxy.type = SOCKS 16 | 17 | when: 18 | def result = proxy.toString() 19 | 20 | then: 21 | !result.contains('thePassword') 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /core/src/test/groovy/org/hidetake/groovy/ssh/core/RemoteSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.core 2 | 3 | import spock.lang.Specification 4 | 5 | class RemoteSpec extends Specification { 6 | 7 | def "remote should be instantiated with settings"() { 8 | given: 9 | def remote = new Remote(name: 'testServer', user: 'admin', host: '1.2.3.4') 10 | 11 | expect: 12 | remote.name == 'testServer' 13 | remote.user == 'admin' 14 | remote.host == '1.2.3.4' 15 | } 16 | 17 | def "name should be generated if name of settings is not given"() { 18 | given: 19 | def remote = new Remote(user: 'admin', host: '1.2.3.4') 20 | 21 | expect: 22 | remote.name =~ /^Remote\d+$/ 23 | remote.user == 'admin' 24 | remote.host == '1.2.3.4' 25 | } 26 | 27 | def "result of toString() does not contain any credential"() { 28 | given: 29 | def remote = new Remote('theRemote') 30 | remote.user = 'theUser' 31 | remote.password = 'thePassword' 32 | remote.identity = new File('theIdentity') 33 | remote.passphrase = 'thePassphrase' 34 | 35 | when: 36 | def result = remote.toString() 37 | 38 | then: 39 | !result.contains('thePassword') 40 | !result.contains('thePassphrase') 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /core/src/test/groovy/org/hidetake/groovy/ssh/interaction/InteractionHandlerSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.interaction 2 | 3 | import spock.lang.Specification 4 | 5 | class InteractionHandlerSpec extends Specification { 6 | 7 | def 'property _ should be the wildcard'() { 8 | expect: 9 | InteractionHandler._ instanceof Wildcard 10 | } 11 | 12 | def 'property standardOutput should be a stream kind'() { 13 | expect: 14 | InteractionHandler.standardOutput instanceof Stream 15 | } 16 | 17 | def 'property standardError should be a stream kind'() { 18 | expect: 19 | InteractionHandler.standardError instanceof Stream 20 | } 21 | 22 | def 'property standardInput should be an output stream given by constructor'() { 23 | given: 24 | def standardInputMock = Mock(OutputStream) 25 | def interactionHandler = new InteractionHandler(standardInputMock) 26 | 27 | expect: 28 | interactionHandler.standardInput == standardInputMock 29 | } 30 | 31 | def 'rules should be empty at first'() { 32 | given: 33 | def interactionHandler = new InteractionHandler(Mock(OutputStream)) 34 | 35 | expect: 36 | interactionHandler.when == [] 37 | } 38 | 39 | def 'when() should add an interaction rule'() { 40 | given: 41 | def interactionHandler = new InteractionHandler(Mock(OutputStream)) 42 | 43 | when: 44 | interactionHandler.when(line: 'value') {} 45 | 46 | then: 47 | interactionHandler.when.size() == 1 48 | interactionHandler.when[0].condition == [line: 'value'] 49 | } 50 | 51 | def 'when() should add interaction rules'() { 52 | given: 53 | def interactionHandler = new InteractionHandler(Mock(OutputStream)) 54 | 55 | when: 56 | interactionHandler.when(line: 'value1') {} 57 | interactionHandler.when(partial: 'value3') {} 58 | 59 | then: 60 | interactionHandler.when.size() == 2 61 | interactionHandler.when[0].condition == [line: 'value1'] 62 | interactionHandler.when[1].condition == [partial: 'value3'] 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /core/src/test/groovy/org/hidetake/groovy/ssh/session/SessionHandlerSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session 2 | 3 | import org.hidetake.groovy.ssh.core.settings.GlobalSettings 4 | import org.hidetake.groovy.ssh.core.settings.PerServiceSettings 5 | import org.hidetake.groovy.ssh.operation.Operations 6 | import spock.lang.Specification 7 | 8 | class SessionHandlerSpec extends Specification { 9 | 10 | def defaultSessionHandler 11 | Operations operations 12 | 13 | def setup() { 14 | operations = Mock(Operations) 15 | defaultSessionHandler = SessionHandler.create(operations, new GlobalSettings(), new PerServiceSettings()) 16 | } 17 | 18 | def "sftp should return value of the closure"() { 19 | given: 20 | def closure = Mock(Closure) 21 | 22 | when: 23 | def result = defaultSessionHandler.with { 24 | sftp(closure) 25 | } 26 | 27 | then: 28 | 1 * operations.sftp(_, closure) >> 'something' 29 | 30 | then: 31 | result == 'something' 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /core/src/test/groovy/org/hidetake/groovy/ssh/session/SessionSettingsSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session 2 | 3 | import spock.lang.Specification 4 | import spock.lang.Unroll 5 | 6 | class SessionSettingsSpec extends Specification { 7 | 8 | @Unroll 9 | def "plus should be merge extensions as list"() { 10 | given: 11 | def settings1 = new SessionSettings.With(extensions: extensions1) 12 | def settings2 = new SessionSettings.With(extensions: extensions2) 13 | 14 | when: 15 | def settings = new SessionSettings.With(settings1, settings2) 16 | 17 | then: 18 | settings.extensions == expected 19 | 20 | where: 21 | extensions1 | extensions2 | expected 22 | [] | [] | [] 23 | ['a'] | [] | ['a'] 24 | [] | ['b'] | ['b'] 25 | ['a'] | ['b'] | ['a', 'b'] 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /core/src/test/groovy/org/hidetake/groovy/ssh/session/execution/EscapeSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.session.execution 2 | 3 | import spock.lang.Specification 4 | import spock.lang.Unroll 5 | 6 | class EscapeSpec extends Specification { 7 | 8 | @Unroll 9 | def "escape() should return escaped string when #args"() { 10 | when: 11 | def escaped = Escape.escape(args) 12 | 13 | then: 14 | escaped == expected 15 | 16 | where: 17 | args | expected 18 | [] | '' 19 | [''] | /''/ 20 | [/hello/, /!"#$%&'()*+,-.\/:;<=>?@[\]^_`{|}~/] | /'hello' '!"#$%&'\''()*+,-.\/:;<=>?@[\]^_`{|}~'/ 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /docs/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.asciidoctor.jvm.convert' version '2.4.0' 3 | } 4 | 5 | asciidoctor { 6 | sources { 7 | include 'index.adoc' 8 | include 'example-script.adoc' 9 | } 10 | } -------------------------------------------------------------------------------- /docs/src/docs/asciidoc/example-script.adoc: -------------------------------------------------------------------------------- 1 | = Example 2 | :doctype: article 3 | :source-highlighter: coderay 4 | 5 | 6 | include::version-loader.adoc[] 7 | 8 | 9 | [source,groovy,subs="+attributes"] 10 | ---- 11 | // build.gradle 12 | 13 | plugins { 14 | id 'org.hidetake.ssh' version '{gradle-ssh-version}' 15 | } 16 | 17 | remotes { 18 | webServer { 19 | host = '192.168.1.101' 20 | user = 'jenkins' 21 | identity = file('id_rsa') 22 | } 23 | } 24 | 25 | task deploy { 26 | doLast { 27 | ssh.run { 28 | session(remotes.webServer) { 29 | put from: 'example.war', into: '/webapps' 30 | execute 'sudo service tomcat restart' 31 | } 32 | } 33 | } 34 | } 35 | ---- 36 | -------------------------------------------------------------------------------- /docs/src/docs/asciidoc/index.adoc: -------------------------------------------------------------------------------- 1 | = Gradle SSH Plugin Document 2 | Hidetake Iwata 3 | :doctype: book 4 | :source-highlighter: coderay 5 | :toc: right 6 | :sectnums: 7 | :sectanchors: 8 | :icons: font 9 | 10 | 11 | include::version-loader.adoc[] 12 | 13 | 14 | This document explains Gradle SSH Plugin {gradle-ssh-version} and Groovy SSH {groovy-ssh-version}. 15 | 16 | 17 | include::introduction.adoc[] 18 | 19 | include::getting-started.adoc[] 20 | 21 | include::user-guide.adoc[] 22 | 23 | include::migration-guide.adoc[] 24 | 25 | 26 | ++++ 27 | 28 | ++++ 29 | -------------------------------------------------------------------------------- /docs/src/docs/asciidoc/introduction.adoc: -------------------------------------------------------------------------------- 1 | = Introduction 2 | 3 | == What is Gradle SSH Plugin 4 | 5 | Gradle SSH Plugin is a https://gradle.org/[Gradle] plugin which provides SSH facilities such as command execution or file transfer for continuous delivery. 6 | It internally uses the library of Groovy SSH. 7 | 8 | 9 | == What is Groovy SSH 10 | 11 | Groovy SSH is an automation tool which provides SSH facilities such as command execution or file transfer. 12 | It is provided as the executable JAR `gssh.jar` and the library `groovy-ssh-{groovy-ssh-version}.jar`. 13 | -------------------------------------------------------------------------------- /docs/src/docs/asciidoc/version-loader.adoc: -------------------------------------------------------------------------------- 1 | :groovy-ssh-version: pass:[x.y.z] 2 | :gradle-ssh-version: pass:[x.y.z] 3 | 4 | ++++ 5 | 6 | 15 | ++++ 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/int128/groovy-ssh/2572bacef5d1b97f3b32a3e62dfb6c40ff547487/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /os-integration-test/bin/.gitignore: -------------------------------------------------------------------------------- 1 | /test/ 2 | /main/ 3 | -------------------------------------------------------------------------------- /os-integration-test/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'groovy' 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | dependencies { 10 | implementation project(':core') 11 | 12 | implementation 'junit:junit:4.13.2' 13 | implementation 'org.spockframework:spock-core:2.3-groovy-3.0' 14 | } 15 | 16 | test { 17 | mustRunAfter ':server-integration-test:check' 18 | } 19 | 20 | task startSshAgent(type: Exec) { 21 | commandLine 'ssh-agent' 22 | standardOutput = new ByteArrayOutputStream() 23 | doLast { 24 | standardOutput.toString().eachMatch(~/(.+?)=(.+?);/) { all, k, v -> 25 | assert k in ['SSH_AGENT_PID', 'SSH_AUTH_SOCK'] 26 | [test, stopSshAgent]*.environment(k, v) 27 | } 28 | } 29 | } 30 | 31 | task stopSshAgent(type: Exec) { 32 | commandLine 'ssh-agent', '-k' 33 | } 34 | 35 | test.dependsOn startSshAgent 36 | test.finalizedBy stopSshAgent 37 | -------------------------------------------------------------------------------- /os-integration-test/etc/ssh/id_ecdsa: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MIIBaAIBAQQg2d776WhdQFfLXan9bhU/bcSaf6HZAv27Js851vDZMImggfowgfcC 3 | AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA//////////////// 4 | MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr 5 | vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE 6 | axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W 7 | K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8 8 | YyVRAgEBoUQDQgAESZT7K4plmvEATrWFhPWybRw4ptSJ7l3rXNc/Dh6GBaLDJli0 9 | SYf6kOaH8xl/EHpbxBTE6P8KVJga1uesO4WbVg== 10 | -----END EC PRIVATE KEY----- 11 | -------------------------------------------------------------------------------- /os-integration-test/etc/ssh/id_ecdsa.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmU+yuKZZrxAE61hYT1sm0cOKbUie5d61zXPw4ehgWiwyZYtEmH+pDmh/MZfxB6W8QUxOj/ClSYGtbnrDuFm1Y= 2 | -------------------------------------------------------------------------------- /os-integration-test/etc/ssh/id_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA78p/+WebOE48aQkyU9HLCezHgEXIVs6aXKH0rgBmlnmNIEf7 3 | y8TWdQ7qQT1esexhmGSpCj/p9iSfeWTv0wG7T0H66toUtuB7ZlV3iEDYXSeYBYKZ 4 | b+cABDhl9bfFcuLR5XDBrTGKfibyLqfGIh1uronPUDGhOf90Ng6G5ZQEK9e8g4BF 5 | PXGrUe9BSqorHppTlGmHPSkFmt4GfHeqx7X2gCNR5yulO6x/Wc15no8r0rejYU+4 6 | kNsiTBdBzqXUJpw6sSa+Yznlq4H9E28FtADd0166ixaVHctgwlhdFxDDmi2YSDTF 7 | dcsUoWAFM4am5z9L6CqCr/CGkhv4bSSgFSF5/QIDAQABAoIBAFKaP1t7BU1wJf9I 8 | 271kF71jg5X8c/bzVNl0MQV/vdc4KBVmtqaLOBU6/hdbPLOt6jDE/DY7rizMkOMQ 9 | kkzt28iBwh4E4f3ddqTZ7ENTkzUD3qqHQrP5r1fE1dq/Y5Uf7Y5MOWugFUU/xU2t 10 | HePCn84gSvolHpUMGsxEVNPhGU7AZ8RBXLYi27ha3OWCqusDJJbQEYRO1xm+SV2t 11 | OzybhEEwYTwmNkPC/1pNyRu3zoG1h+tGR2t1yOK1n35ygRiL0lxDHmJ19sRxJ1vn 12 | jKAKU8hnjG1L/uQxVpleOt5TQhIq7wWLL7jO+EMYu5w+iptHGeRs+j/15jY3LFus 13 | V/3v+UECgYEA/MEKUzgl34YN8jMBS3/rzvp+C/4U0nUMyM5+SLa4slYxtL5QbLLu 14 | dPXUTlFSXxUa8OypzM0HqyyWzvXFiQ05Te63wS83rds9/ox6WHAfcwFnkJLIuqCR 15 | N2dNbD0fLV2ixtjNJLsPmVL3upY9/rSYLz9uvw/7/nW/O3AzpVaSbssCgYEA8t7X 16 | i21C4oeWUXJhYnVWWMNqNSoHXsaBMcA5U82aA24cCpoqqRcPpkVYBOlwksGOzJKa 17 | 0rUpaiUknoaqO17o4Q5kKfQYbfGPJ5BnED2iDeVufbyEgfLK0lQX9LNR7Cxtxos6 18 | wSkOvyNllauu6CHCoYSM+EZTiCekAv0GKlgkGVcCgYB9skLAQBwVnUUyPctXELbk 19 | qA4nSKRyRWOmOYrz/mq7xcHScRLt+846vEZo7GhagNR1HD0VbKFzrykQo4kpLzpg 20 | V2dq22CFRZL/FD2D3b7GItyuOVE5/sA5HVaTjZIDrZ1V5lue+Kg5R9mLIUyTbpyA 21 | YrtgqUJYuZXwqUwF3ZfVIQKBgCpxcR+nl4G5Cjbvkz8+nDlk5SGnV6Rjcl58ZkhT 22 | 7O9ehb4AlSX5pr167tfk58xt0QPFNxNNn5AyL4UYqZU4j+AMwMpoIwDLryXN4YUA 23 | EFr3VmjY0htXj8RT99/GmrF4TjLdUAZDo5UZnX4bg7SDedz6KhyVRbHMo6f2CebK 24 | gnx/AoGBAPMFUEa5Tgq7V4XgW6PLHA3SfXj7yNrE72V3aAVkVgkuGAbiOH08cOOF 25 | j9GBq+5NyawyVmVeOe9c/vZG8tPeEFl4550x7n69iCfl7Jaraf1JJF9aEDZprIqE 26 | w6LAtQVE3oH8HvbUEx6dNTMwo5e82tVei8PS1ez6qPfB9CiEns7S 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /os-integration-test/etc/ssh/id_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDvyn/5Z5s4TjxpCTJT0csJ7MeARchWzppcofSuAGaWeY0gR/vLxNZ1DupBPV6x7GGYZKkKP+n2JJ95ZO/TAbtPQfrq2hS24HtmVXeIQNhdJ5gFgplv5wAEOGX1t8Vy4tHlcMGtMYp+JvIup8YiHW6uic9QMaE5/3Q2DobllAQr17yDgEU9catR70FKqisemlOUaYc9KQWa3gZ8d6rHtfaAI1HnK6U7rH9ZzXmejyvSt6NhT7iQ2yJMF0HOpdQmnDqxJr5jOeWrgf0TbwW0AN3TXrqLFpUdy2DCWF0XEMOaLZhINMV1yxShYAUzhqbnP0voKoKv8IaSG/htJKAVIXn9 2 | -------------------------------------------------------------------------------- /os-integration-test/etc/ssh/id_rsa_pass: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: AES-128-CBC,849C5178C9A9417A297428134CEAA4A5 4 | 5 | WhtJT/g5q1Sz27hvJXOa9RWRakWYj3Ndi2/dpxWdXaWxoNaZkJr3xxqeegG797ze 6 | gLlQlPFZRJU9KGS3+JxLxV9lyRr16hHllWtg1gOOMniYMafoabK2lxOYzLsc2jUq 7 | 5B1XqKDgwHfI9Jg1dCvZsyac+c6vv7BkzrwVVZj92C4IZl/YtmWKkNXOEbyJKNoq 8 | ONGoHV3DhVQbTK1qELMyh+H7CQCZdLPjwSn5X88x/wQeSpTvy3oOdcYwYXDeQfoM 9 | n87yxnc4Rw/pOj2HL0SXqx+FAzi3HW3ZIqL0JDfXhJPnSVTukkKhPsx1lljSgQcz 10 | ERTHlU2M+arXfUK+nnyw5wdqiTmXG2mhjGhN1wSfNZnlFTZDYAUVMlZ4I4WnlFJ2 11 | CyzlI1cP5heNS/+Pa5dDvTyeHOG/ciOeyAtn4F/lqv3muayjKLG2dIq6xni4yNZp 12 | /ZrAkH2zsHtBB7yzrRi7/Nh6n3YezTS/yR7kqlTKEByr2JccXdmUTNf1H42H+iOH 13 | 4qekj7Qo/qOo1YwANKomkci6zEf6/EUDVvyl0P8GGh/0X6QI2o12ZOLVW4m+TD+Z 14 | IB4Nfl8QHTTfD6rkJUgIAjdqxY5nBhaFRKwsMLJx4Vstx4LIO5qHlRTYXViIt2DC 15 | eBmvxvJ5p9pRXidt6r02JcXckJ+LxWWZDViS8RPG1qsYxpCgwGcBrPMc3MZKAHxu 16 | /V61PzTdZ3e61Us7touIm6cRfa48gC0vOK86pLbAd1GyJxm/F5GwsuPNdYguw5TV 17 | sLnTPZEehdDmxadsU5fjwjbf5+85LmLj/tRKvtM10avhv/Z4lIjF8AEv16I9kzKE 18 | rd+F/sj2jpVXH3alkuCc5GCh8Pk/LTSothvElfwneiNDsQloROaW3Lz8RFOalkvs 19 | qxc8p4VrHkotp5Y96exNqS6xmojTCuX5r8ZIdmJZMo4DRWbQtNHdk2tJwlLIxTIH 20 | KjQHzBHLoeSGJOZjLbrk6y7m+9LZvkb6HksiYksGQ6WBxcEn/UYZssGHkmvo/UrN 21 | OvMSzUP091zkA23t6LtCPH00VXBTvNWWKPtPE+ySJEmu2cyG8Y7bGqV58SYM/aKs 22 | +zzfngYnqdg06D3V1r1tnoKWwEdNua/rYOEIeu5zvzMUAQDqPc6Tfjj1aBsAtTJv 23 | vTEWWIrtrC8Edjj3dOF4S0fZNtD4mqzfx4n4elFjvJq9u6cC+Ull36vSe2oINRiF 24 | DSpT+3MZDMapqaDdxSqJ/waKRrkUzXivSJZ1o4reu/TMtB5WdL8839PNURoQFNpK 25 | lqDxB7YzuqAgQugF1NtjEOPYaR5KLBO0NjSSa/AqQ8LIAEBUDbmX7+Dl5xfXX1gk 26 | NLtZ9sccXPoKONn0V8Q0NuCvquci1M3pdMNtXXXrfQDgRfaWOev2zB+62tuaab1n 27 | DOB91WXGPREceJqX56W035BKt+5m3dDyQHX12pL3vD0uWW222nSF3Bm0rFPjZQuV 28 | 1so1D/HSnUifLbAKoICmSpwhF62S5Ygfb5RKbqP5sp24Q3jtt/v0Bc+YuOCaSOR7 29 | yQSEEaV4SNdjPIOR+9qe1qlGfCAqQRxrVfKTP5k1Yp4Hhf+jF0AQQx2I4Ctox0Qf 30 | -----END RSA PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /os-integration-test/etc/ssh/id_rsa_pass.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1xN7CzvOi8IOUqXs+y7BvCwr52enPvcaObR5ymVMZKwPiXXqNKOnvQsKv23JxSOfoHS7WeWqTCkxrawsTSB6ZO7qy6AHxF1qa26KGuwkgbAa7wZIAQtrAH/li04jllxDKYdcriq76poVVvK76q6ASxAy76eboWDrABBXhNWD1pHZj5pE0vKpdGRnAqe/vWIkkmJLy5cVwcrKz8RThtTYv1w5Wuzp2z4W7+O9oFXvYr76lztxLSMtgGcoM6hRUdn+CfBP4aN8LBr0MnvAcQec2aZUOEBFl8q73ZRQ10SmZciIaLEBI/MrB0Oa/TY48/FcQjwaFmQdyp2AIG/tQdzmx your_email@example.com 2 | -------------------------------------------------------------------------------- /os-integration-test/etc/ssh/known_hosts: -------------------------------------------------------------------------------- 1 | localhost ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNvZCmB74GsbDpIp+LaVPO5tQ9LkViA4YkE5XciA0Yk4/ir6VBlXgpAKr5eC0Owvc7EA4XJr+xrd00j3RKHlleg= 2 | -------------------------------------------------------------------------------- /os-integration-test/etc/ssh/ssh_host_dsa_key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABswAAAAdzc2gtZH 3 | NzAAAAgQDB5zSCWJEMYMspTanQt0tRR4JhDaDASR+RL84DfQX7Q1XuPG2YpHslB2G0D530 4 | 4xPG7KdbpaTH3zZS8e6WfGKahtSX0+gUZWwpECin/UZdjDiXNz6XJ1ZEoCRLkid+yArXi6 5 | 3BcDcNoPPNwyyMpvR9kNmGprh9G7qUtWDMYNul0wAAABUAjde0V4ohDO7KYPO4drK3AuIm 6 | vfEAAACBAMBFlUuR/RekINTvSbl1+5/iHYv4BULhCSYtTNuOMfidizLIYTDd0uzDxGZwf3 7 | SumhU6j74ew221mBK9C6y0m3v6lcOfa6M+QGrmaI2ciXDSmpxZYdT+RDpMcOZsURAY087F 8 | hxXbLSjCnBzfBfx8vJLOCaQs5qy6HAVm34z8YNeqAAAAgQCWuDOZWIaKghws6Pa6yoyZA9 9 | MMyhgV7HM6KQnAP0XQ+33dRsUVsiCgtgBL/uImkFtOJhrsHEA0pHOG4Cor9Hzyo4Ur57ln 10 | 7kR2IUxhPKm4OyW3meH4gcAVBN1jYnmwZqRgF+7hi6tZNhEzr7VG6N92LpAQ+6Rao4QEAo 11 | m8DnT5yAAAAfDnggvs54IL7AAAAAdzc2gtZHNzAAAAgQDB5zSCWJEMYMspTanQt0tRR4Jh 12 | DaDASR+RL84DfQX7Q1XuPG2YpHslB2G0D5304xPG7KdbpaTH3zZS8e6WfGKahtSX0+gUZW 13 | wpECin/UZdjDiXNz6XJ1ZEoCRLkid+yArXi63BcDcNoPPNwyyMpvR9kNmGprh9G7qUtWDM 14 | YNul0wAAABUAjde0V4ohDO7KYPO4drK3AuImvfEAAACBAMBFlUuR/RekINTvSbl1+5/iHY 15 | v4BULhCSYtTNuOMfidizLIYTDd0uzDxGZwf3SumhU6j74ew221mBK9C6y0m3v6lcOfa6M+ 16 | QGrmaI2ciXDSmpxZYdT+RDpMcOZsURAY087FhxXbLSjCnBzfBfx8vJLOCaQs5qy6HAVm34 17 | z8YNeqAAAAgQCWuDOZWIaKghws6Pa6yoyZA9MMyhgV7HM6KQnAP0XQ+33dRsUVsiCgtgBL 18 | /uImkFtOJhrsHEA0pHOG4Cor9Hzyo4Ur57ln7kR2IUxhPKm4OyW3meH4gcAVBN1jYnmwZq 19 | RgF+7hi6tZNhEzr7VG6N92LpAQ+6Rao4QEAom8DnT5yAAAABQcGgS2uRxbrfvcxPwmL/Mq 20 | JOcO3QAAABVoaWRldGFrZUByYWJiaXQubG9jYWwBAgME 21 | -----END OPENSSH PRIVATE KEY----- 22 | -------------------------------------------------------------------------------- /os-integration-test/etc/ssh/ssh_host_dsa_key.pub: -------------------------------------------------------------------------------- 1 | ssh-dss AAAAB3NzaC1kc3MAAACBAMHnNIJYkQxgyylNqdC3S1FHgmENoMBJH5EvzgN9BftDVe48bZikeyUHYbQPnfTjE8bsp1ulpMffNlLx7pZ8YpqG1JfT6BRlbCkQKKf9Rl2MOJc3PpcnVkSgJEuSJ37ICteLrcFwNw2g883DLIym9H2Q2YamuH0bupS1YMxg26XTAAAAFQCN17RXiiEM7spg87h2srcC4ia98QAAAIEAwEWVS5H9F6Qg1O9JuXX7n+Idi/gFQuEJJi1M244x+J2LMshhMN3S7MPEZnB/dK6aFTqPvh7DbbWYEr0LrLSbe/qVw59roz5AauZojZyJcNKanFlh1P5EOkxw5mxREBjTzsWHFdstKMKcHN8F/Hy8ks4JpCzmrLocBWbfjPxg16oAAACBAJa4M5lYhoqCHCzo9rrKjJkD0wzKGBXsczopCcA/RdD7fd1GxRWyIKC2AEv+4iaQW04mGuwcQDSkc4bgKiv0fPKjhSvnuWfuRHYhTGE8qbg7JbeZ4fiBwBUE3WNiebBmpGAX7uGLq1k2ETOvtUbo33YukBD7pFqjhAQCibwOdPnI 2 | -------------------------------------------------------------------------------- /os-integration-test/etc/ssh/ssh_host_ecdsa_key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS 3 | 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTb2Qpge+BrGw6SKfi2lTzubUPS5FYg 4 | OGJBOV3IgNGJOP4q+lQZV4KQCq+XgtDsL3OxAOFya/sa3dNI90Sh5ZXoAAAAsOBC60DgQu 5 | tAAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNvZCmB74GsbDpIp 6 | +LaVPO5tQ9LkViA4YkE5XciA0Yk4/ir6VBlXgpAKr5eC0Owvc7EA4XJr+xrd00j3RKHlle 7 | gAAAAgQhtKq9f2GGDeivbuZ3tPKWtzabtWWkBBMCP79B1WIhcAAAAVaGlkZXRha2VAcmFi 8 | Yml0LmxvY2FsAQID 9 | -----END OPENSSH PRIVATE KEY----- 10 | -------------------------------------------------------------------------------- /os-integration-test/etc/ssh/ssh_host_ecdsa_key.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNvZCmB74GsbDpIp+LaVPO5tQ9LkViA4YkE5XciA0Yk4/ir6VBlXgpAKr5eC0Owvc7EA4XJr+xrd00j3RKHlleg= 2 | -------------------------------------------------------------------------------- /os-integration-test/etc/ssh/ssh_host_ed25519_key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACAg0uc6mInMIv6tF/4rYU5Tn/VBZ0Wp7HSzBqMQDLwPqgAAAJi6uxPeursT 4 | 3gAAAAtzc2gtZWQyNTUxOQAAACAg0uc6mInMIv6tF/4rYU5Tn/VBZ0Wp7HSzBqMQDLwPqg 5 | AAAEDtQZ8yklhUquVEgewNF+kCFYADJ5vOQqrAfqWZ1XpiryDS5zqYicwi/q0X/ithTlOf 6 | 9UFnRansdLMGoxAMvA+qAAAAFWhpZGV0YWtlQHJhYmJpdC5sb2NhbA== 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /os-integration-test/etc/ssh/ssh_host_ed25519_key.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICDS5zqYicwi/q0X/ithTlOf9UFnRansdLMGoxAMvA+q 2 | -------------------------------------------------------------------------------- /os-integration-test/etc/ssh/ssh_host_rsa_key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn 3 | NhAAAAAwEAAQAAAQEAvQA/tTTYSOslnkBwHcQ3dQ+09adHECgRQk0y9+3PZv4EQaN48MzF 4 | hlqjLM8RUfKnoIVp3yHdo0uEZaDYWD5nyePSnOihIV0sx5gOoJdGhzligcQnhhC2EKoBOy 5 | u23NnT7Qx+ObgLxZYD28rsYD5WREp5Xj8Lv7BdQzuH3z+4l0CTOrSYok2dP+InqMBBiR+k 6 | k1UXOqcD6YeipIuzlrhqOceN07zON/dm9RGOY040So3mCKcn0TJ2wYB+bHLkimC6kgaiCy 7 | TWIGI/7yiwTuYDJYhAqCTPaaABoz6k4LdDr9+7mjTADRQseFf7VcKFT7kiSfbrUNSkGxKQ 8 | +Mw/w7EfgQAAA9Cj8Rn7o/EZ+wAAAAdzc2gtcnNhAAABAQC9AD+1NNhI6yWeQHAdxDd1D7 9 | T1p0cQKBFCTTL37c9m/gRBo3jwzMWGWqMszxFR8qeghWnfId2jS4RloNhYPmfJ49Kc6KEh 10 | XSzHmA6gl0aHOWKBxCeGELYQqgE7K7bc2dPtDH45uAvFlgPbyuxgPlZESnlePwu/sF1DO4 11 | ffP7iXQJM6tJiiTZ0/4ieowEGJH6STVRc6pwPph6Kki7OWuGo5x43TvM4392b1EY5jTjRK 12 | jeYIpyfRMnbBgH5scuSKYLqSBqILJNYgYj/vKLBO5gMliECoJM9poAGjPqTgt0Ov37uaNM 13 | ANFCx4V/tVwoVPuSJJ9utQ1KQbEpD4zD/DsR+BAAAAAwEAAQAAAQEAhfWOMi6ReiWJFUCQ 14 | 9tgjgooudcspmC7+BKNZE9dvoI08kRV/3BUXj6HgdBsUKKQ34ZOONcP4JwyYe7vke69Hux 15 | YKKoLL6izzV0jUXUi7iY7H3jgc124yzV7h3oGea6zNBABN2zUyysoIVBnhLlogpOiwW3eO 16 | KUCk6clhBYBRoom/OpCnvx0K6RW2m9rmj9IdSt5XkIqR+e4idP4f4FMIUprX2fUbCtpyvz 17 | 1bC3B19dhLqi1hnQKHw8UAQIUXzwSlgSmbf/KO56Lm4kN4KsIOiG8jjRqe8y2Zz40j3jfP 18 | ERdjh3Gx8opRb2iSZO2N48dltm+iPHe/pSvNGdz1ZfQAAQAAAIEAt+/7p7AI1vKBHtB8ra 19 | iG8ShaFRD32i5rILVI+UEp7NKi48Xm36LLbxgJZwNGsF4DGcimMfdjyHy7J1aB6BleuKUX 20 | iHRQFus26iU6jW7pqSIG9UAEONxO2smv8AjpU9t6oWmWj91osiuykMWcKrnKYgR7tB+BsS 21 | hIVqJagkn1jdwAAACBAOkRQheAz7c8hDnz3nnTBF0fACL8/yH4igChrYA29L1EdKgH1FwW 22 | RZIix6ROxZunYlbbdLhx2XXJnu07ffROVNku6bBK+XIHyrQlrUEU4xmKzwbmh2sBv78T6u 23 | WqmEj/YfIx7aX0G+i5GWfPhKdwvMJI1ZATEv7OVGo2DO6HoFWBAAAAgQDPmP/yfdxCxtWK 24 | GY1MANtFh/b2xWgSPRasbTg3L3LEk2I3WiQPcARTLv9O02ZNrwBSPJ33BF5GEkUcK13WI3 25 | P3ns3YHA031EPwQrF1T0iHN024EkDl0KKUMjobSOeoC1RmXHuJ6Do3lHHNxlvrkIKw3YxV 26 | 92rSZ1v9fq2p3xnKAQAAABVoaWRldGFrZUByYWJiaXQubG9jYWwBAgME 27 | -----END OPENSSH PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /os-integration-test/etc/ssh/ssh_host_rsa_key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9AD+1NNhI6yWeQHAdxDd1D7T1p0cQKBFCTTL37c9m/gRBo3jwzMWGWqMszxFR8qeghWnfId2jS4RloNhYPmfJ49Kc6KEhXSzHmA6gl0aHOWKBxCeGELYQqgE7K7bc2dPtDH45uAvFlgPbyuxgPlZESnlePwu/sF1DO4ffP7iXQJM6tJiiTZ0/4ieowEGJH6STVRc6pwPph6Kki7OWuGo5x43TvM4392b1EY5jTjRKjeYIpyfRMnbBgH5scuSKYLqSBqILJNYgYj/vKLBO5gMliECoJM9poAGjPqTgt0Ov37uaNMANFCx4V/tVwoVPuSJJ9utQ1KQbEpD4zD/DsR+B 2 | -------------------------------------------------------------------------------- /os-integration-test/run-sshd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | exec docker run --rm -p 22:22 \ 4 | -e "SSH_HOST_DSA_KEY=$(cat etc/ssh/ssh_host_dsa_key)" \ 5 | -e "SSH_HOST_RSA_KEY=$(cat etc/ssh/ssh_host_rsa_key)" \ 6 | -e "SSH_HOST_ECDSA_KEY=$(cat etc/ssh/ssh_host_ecdsa_key)" \ 7 | -e "SSH_HOST_ED25519_KEY=$(cat etc/ssh/ssh_host_ed25519_key)" \ 8 | -e "SSH_AUTHORIZED_KEYS=$(cat etc/ssh/id_rsa.pub etc/ssh/id_rsa_pass.pub etc/ssh/id_ecdsa.pub)" \ 9 | --name sshd \ 10 | int128/sshd 11 | -------------------------------------------------------------------------------- /os-integration-test/src/main/groovy/org/hidetake/groovy/ssh/test/os/FileDivCategory.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.os 2 | 3 | @Category(File) 4 | class FileDivCategory { 5 | 6 | File div(String child) { 7 | new File(this as File, child) 8 | } 9 | 10 | File div(MkdirType type) { 11 | switch (type) { 12 | case MkdirType.DIRECTORY: 13 | assert mkdir() 14 | break 15 | 16 | case MkdirType.DIRECTORIES: 17 | assert mkdirs() 18 | break 19 | 20 | default: 21 | throw new IllegalArgumentException("Unknown mkdir type: $type") 22 | } 23 | this 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /os-integration-test/src/main/groovy/org/hidetake/groovy/ssh/test/os/Fixture.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.os 2 | 3 | import org.hidetake.groovy.ssh.core.Service 4 | 5 | class Fixture { 6 | 7 | static randomInt(int max = 10000) { 8 | (Math.random() * max) as int 9 | } 10 | 11 | static remoteTmpPath() { 12 | "/tmp/groovy-ssh.os-integration-test.${UUID.randomUUID()}" 13 | } 14 | 15 | static createRemotes(Service service) { 16 | service.remotes { 17 | Default { 18 | host = 'localhost' 19 | port = 22 20 | user = 'tester' 21 | identity = new File("etc/ssh/id_rsa") 22 | knownHosts = addHostKey(new File("build/known_hosts")) 23 | } 24 | } 25 | service.remotes { 26 | DefaultWithECDSAKey { 27 | host = service.remotes.Default.host 28 | port = service.remotes.Default.port 29 | user = service.remotes.Default.user 30 | identity = new File("etc/ssh/id_ecdsa") 31 | knownHosts = service.remotes.Default.knownHosts 32 | } 33 | DefaultWithPassphrase { 34 | host = service.remotes.Default.host 35 | port = service.remotes.Default.port 36 | user = service.remotes.Default.user 37 | knownHosts = service.remotes.Default.knownHosts 38 | 39 | identity = new File("etc/ssh/id_rsa_pass") 40 | passphrase = 'gradle' 41 | } 42 | DefaultWithOpenSSHKnownHosts { 43 | host = service.remotes.Default.host 44 | port = service.remotes.Default.port 45 | user = service.remotes.Default.user 46 | identity = service.remotes.Default.identity 47 | 48 | knownHosts = new File("etc/ssh/known_hosts") 49 | } 50 | DefaultWithAgent { 51 | host = service.remotes.Default.host 52 | port = service.remotes.Default.port 53 | user = service.remotes.Default.user 54 | knownHosts = service.remotes.Default.knownHosts 55 | 56 | agent = true 57 | } 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /os-integration-test/src/main/groovy/org/hidetake/groovy/ssh/test/os/MkdirType.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.os 2 | 3 | enum MkdirType { 4 | DIRECTORY, 5 | DIRECTORIES 6 | } 7 | -------------------------------------------------------------------------------- /os-integration-test/src/main/groovy/org/hidetake/groovy/ssh/test/os/SshAgent.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.os 2 | 3 | import groovy.util.logging.Slf4j 4 | import org.junit.rules.ExternalResource 5 | 6 | @Slf4j 7 | class SshAgent extends ExternalResource { 8 | 9 | @Override 10 | protected void before() { 11 | removeAll() 12 | } 13 | 14 | @Override 15 | protected void after() { 16 | removeAll() 17 | } 18 | 19 | void add(String keyPath) { 20 | ['chmod', '600', keyPath].execute().waitForProcessOutput(System.out, System.err) 21 | log.info("Adding key to ssh-agent: $keyPath") 22 | ['ssh-add', keyPath].execute().waitForProcessOutput(System.out, System.err) 23 | } 24 | 25 | void removeAll() { 26 | log.info('Remove all keys from ssh-agent') 27 | ['ssh-add', '-D'].execute().waitForProcessOutput(System.out, System.err) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /os-integration-test/src/main/groovy/org/hidetake/groovy/ssh/test/os/UserManagementExtension.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.os 2 | 3 | import org.hidetake.groovy.ssh.session.SessionExtension 4 | 5 | trait UserManagementExtension implements SessionExtension { 6 | 7 | void recreateUser(String user) { 8 | execute """ 9 | if id "$user"; then 10 | sudo deluser --remove-home $user 11 | fi 12 | sudo adduser -D $user 13 | """, pty: true 14 | } 15 | 16 | void configurePassword(String user, String password) { 17 | execute "sudo passwd $user", pty: true, interaction: Helper.passwordInteraction.curry(password) 18 | } 19 | 20 | static class Helper { 21 | static final passwordInteraction = { password -> 22 | when(partial: ~/.+[Pp]assword: */) { 23 | standardInput << password << '\n' 24 | } 25 | when(line: _) {} 26 | } 27 | } 28 | 29 | void configureAuthorizedKeysAsCurrentUser(String user) { 30 | execute """ 31 | sudo -i -u $user mkdir -m 700 .ssh 32 | sudo -i -u $user touch .ssh/authorized_keys 33 | sudo -i -u $user chmod 600 .ssh/authorized_keys 34 | sudo -i -u $user tee .ssh/authorized_keys < ~/.ssh/authorized_keys > /dev/null 35 | """, pty: true 36 | } 37 | 38 | void configureAuthorizedKeys(String user, String publicKey) { 39 | execute """ 40 | sudo -i -u $user mkdir -m 700 .ssh 41 | sudo -i -u $user touch .ssh/authorized_keys 42 | sudo -i -u $user chmod 600 .ssh/authorized_keys 43 | echo '$publicKey' | sudo -i -u $user tee .ssh/authorized_keys > /dev/null 44 | """, pty: true 45 | } 46 | 47 | void configureSudoers(String content) { 48 | put text: content, into: '/tmp/groovy-ssh-sudoers' 49 | execute """ 50 | sudo chmod 440 /tmp/groovy-ssh-sudoers 51 | sudo chown 0.0 /tmp/groovy-ssh-sudoers 52 | sudo mkdir -p -m 700 /etc/sudoers.d 53 | sudo mv /tmp/groovy-ssh-sudoers /etc/sudoers.d/groovy-ssh-sudoers 54 | """, pty: true 55 | } 56 | 57 | void cleanupSudoers() { 58 | execute 'sudo rm -f /etc/sudoers.d/groovy-ssh-sudoers' 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /os-integration-test/src/test/groovy/org/hidetake/groovy/ssh/test/os/GatewaySpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.os 2 | 3 | import org.hidetake.groovy.ssh.Ssh 4 | import org.hidetake.groovy.ssh.core.Service 5 | import org.junit.Rule 6 | import org.junit.rules.TemporaryFolder 7 | import spock.lang.Ignore 8 | import spock.lang.Specification 9 | 10 | import static org.hidetake.groovy.ssh.test.os.Fixture.createRemotes 11 | 12 | /** 13 | * Check if gateway access should work with Linux system. 14 | * 15 | * @author Hidetake Iwata 16 | */ 17 | class GatewaySpec extends Specification { 18 | 19 | @Rule 20 | TemporaryFolder temporaryFolder 21 | 22 | Service ssh 23 | 24 | def setup() { 25 | ssh = Ssh.newService() 26 | createRemotes(ssh) 27 | ssh.remotes { 28 | InternalServer { 29 | host = 'groovy-ssh-integration-test-internal-box' 30 | user = ssh.remotes.Default.user 31 | identity = ssh.remotes.Default.identity 32 | gateway = ssh.remotes.Default 33 | } 34 | } 35 | } 36 | 37 | //FIXME: at this time no way to test multi-hop on CircleCI 38 | @Ignore 39 | def "it should connect to target server via gateway server"() { 40 | given: 41 | def knownHostsFile = temporaryFolder.newFile() 42 | 43 | when: 44 | ssh.run { 45 | settings { 46 | knownHosts = addHostKey(knownHostsFile) 47 | } 48 | session(ssh.remotes.InternalServer) { 49 | execute 'hostname' 50 | } 51 | } 52 | 53 | then: 54 | knownHostsFile.text =~ /^groovy-ssh-integration-test-internal-box .+/ 55 | 56 | when: 57 | ssh.run { 58 | settings { 59 | knownHosts = knownHostsFile 60 | } 61 | session(ssh.remotes.InternalServer) { 62 | execute 'hostname' 63 | } 64 | } 65 | 66 | then: 67 | knownHostsFile.text =~ /^groovy-ssh-integration-test-internal-box .+/ 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /os-integration-test/src/test/groovy/org/hidetake/groovy/ssh/test/os/HostAuthenticationSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.os 2 | 3 | import org.hidetake.groovy.ssh.Ssh 4 | import org.hidetake.groovy.ssh.core.Service 5 | import spock.lang.Specification 6 | 7 | import static org.hidetake.groovy.ssh.test.os.Fixture.createRemotes 8 | import static org.hidetake.groovy.ssh.test.os.Fixture.randomInt 9 | 10 | /** 11 | * Check if host authentication works with real OS environment. 12 | * 13 | * @author Hidetake Iwata 14 | */ 15 | class HostAuthenticationSpec extends Specification { 16 | 17 | Service ssh 18 | 19 | def setup() { 20 | ssh = Ssh.newService() 21 | createRemotes(ssh) 22 | } 23 | 24 | def 'should work with known_hosts generated by OpenSSH'() { 25 | given: 26 | def x = randomInt() 27 | def y = randomInt() 28 | 29 | when: 30 | def r = ssh.run { 31 | session(ssh.remotes.DefaultWithOpenSSHKnownHosts) { 32 | execute "expr $x + $y" 33 | } 34 | } as int 35 | 36 | then: 37 | r == (x + y) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /os-integration-test/src/test/groovy/org/hidetake/groovy/ssh/test/os/ScpSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.os 2 | 3 | import org.hidetake.groovy.ssh.session.transfer.FileTransferMethod 4 | import spock.lang.Timeout 5 | 6 | /** 7 | * Check if file transfer works with SCP command of OpenSSH. 8 | * 9 | * @author Hidetake Iwata 10 | */ 11 | @Timeout(10) 12 | class ScpSpec extends AbstractFileTransferSpec { 13 | 14 | def setup() { 15 | ssh.settings { 16 | fileTransfer = FileTransferMethod.scp 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /os-integration-test/src/test/groovy/org/hidetake/groovy/ssh/test/os/ScriptSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.os 2 | 3 | import org.codehaus.groovy.tools.Utilities 4 | import org.hidetake.groovy.ssh.Ssh 5 | import org.hidetake.groovy.ssh.core.Service 6 | import spock.lang.Specification 7 | 8 | import static org.hidetake.groovy.ssh.test.os.Fixture.createRemotes 9 | 10 | class ScriptSpec extends Specification { 11 | 12 | Service ssh 13 | 14 | def setup() { 15 | ssh = Ssh.newService() 16 | createRemotes(ssh) 17 | } 18 | 19 | def 'should execute a script'() { 20 | when: 21 | def actual = ssh.run { 22 | session(ssh.remotes.Default) { 23 | executeScript '''#!/bin/sh -xe 24 | echo 1 25 | echo 2 26 | ''' 27 | } 28 | } 29 | 30 | then: 31 | actual == "1${Utilities.eol()}2" 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /os-integration-test/src/test/groovy/org/hidetake/groovy/ssh/test/os/SftpSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.os 2 | 3 | import static org.hidetake.groovy.ssh.test.os.Fixture.remoteTmpPath 4 | 5 | /** 6 | * Check if file transfer works with SFTP subsystem of OpenSSH. 7 | * 8 | * @author Hidetake Iwata 9 | */ 10 | class SftpSpec extends AbstractFileTransferSpec { 11 | 12 | def 'remove() should delete a directory recursively'() { 13 | given: 14 | def remoteDir = remoteTmpPath() 15 | 16 | when: 17 | ssh.run { 18 | session(ssh.remotes.Default) { 19 | execute "mkdir -vp $remoteDir/foo/bar" 20 | execute "date > $remoteDir/foo/bar/baz" 21 | remove remoteDir 22 | execute "test ! -d $remoteDir" 23 | } 24 | } 25 | 26 | then: 27 | noExceptionThrown() 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /os-integration-test/src/test/groovy/org/hidetake/groovy/ssh/test/os/ShellSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.os 2 | 3 | import org.hidetake.groovy.ssh.Ssh 4 | import org.hidetake.groovy.ssh.core.Service 5 | import spock.lang.Ignore 6 | import spock.lang.Specification 7 | import spock.lang.Timeout 8 | 9 | import static org.hidetake.groovy.ssh.test.os.Fixture.createRemotes 10 | 11 | @Timeout(10) 12 | class ShellSpec extends Specification { 13 | 14 | Service ssh 15 | 16 | def setup() { 17 | ssh = Ssh.newService() 18 | createRemotes(ssh) 19 | } 20 | 21 | //FIXME: do not work with Alpine 22 | @Ignore 23 | def 'should execute the shell'() { 24 | when: 25 | ssh.run { 26 | session(ssh.remotes.Default) { 27 | shell(interaction: { 28 | when(partial: ~/.*[$%#]\W*/, from: standardOutput) { 29 | standardInput << 'uname -a' << '\n' 30 | 31 | when(partial: ~/.*[$%#]\W*/, from: standardOutput) { 32 | standardInput << 'exit 0' << '\n' 33 | } 34 | when(line: _, from: standardOutput) {} 35 | } 36 | when(line: _, from: standardOutput) {} 37 | }) 38 | } 39 | } 40 | 41 | then: 42 | noExceptionThrown() 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /os-integration-test/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /plugin-integration/create-branch-for-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | cd "$(dirname $0)/gradle-ssh-plugin" 4 | git reset --hard 5 | 6 | git checkout -b "groovy-ssh-$CIRCLE_TAG" 7 | sed -i -e "s,groovy-ssh:[0-9.]*,groovy-ssh:${CIRCLE_TAG:-SNAPSHOT},g" core/build.gradle 8 | git add . 9 | git commit -m "Groovy SSH $CIRCLE_TAG" -m "https://github.com/int128/groovy-ssh/releases/tag/$CIRCLE_TAG" 10 | git push origin 11 | -------------------------------------------------------------------------------- /plugin-integration/run-plugin-integration-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | function checkout_remote_branch () { 4 | local branch_name="$1" 5 | git fetch origin -v "$branch_name:$branch_name" 6 | git checkout "$branch_name" 7 | } 8 | 9 | cd "$(dirname $0)/gradle-ssh-plugin" 10 | git reset --hard 11 | 12 | if checkout_remote_branch groovy-ssh-acceptance-test 13 | then 14 | echo 'Use dedicated branch for specification change breaking backward compatibility' 15 | fi 16 | 17 | sed -i -e "s,groovy-ssh:[0-9.]*,groovy-ssh:${CIRCLE_TAG:-SNAPSHOT},g" core/build.gradle 18 | echo 'repositories.mavenLocal()' >> core/build.gradle 19 | 20 | mkdir -p acceptance-test/fixture/build 21 | cp -av ../../os-integration-test/build/.ssh acceptance-test/fixture/build/.ssh 22 | 23 | ./gradlew -Ptarget.gradle.versions=1.12 :acceptance-test:test 24 | -------------------------------------------------------------------------------- /server-integration-test/bin/.gitignore: -------------------------------------------------------------------------------- 1 | /main/ 2 | /test/ 3 | /default/ 4 | -------------------------------------------------------------------------------- /server-integration-test/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'groovy' 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | dependencies { 10 | implementation project(':core') 11 | implementation 'org.apache.sshd:sshd-core:2.2.0' 12 | implementation 'org.apache.sshd:sshd-sftp:2.2.0' 13 | implementation 'org.apache.sshd:sshd-scp:2.2.0' 14 | 15 | runtimeOnly 'org.bouncycastle:bcpkix-jdk15on:1.70' 16 | 17 | testImplementation 'org.codehaus.groovy.modules.http-builder:http-builder:0.7.1' 18 | testImplementation platform("org.spockframework:spock-bom:2.3-groovy-3.0") 19 | testImplementation "org.spockframework:spock-core" 20 | testImplementation "org.spockframework:spock-junit4" 21 | testRuntimeOnly 'ch.qos.logback:logback-classic:1.5.18' 22 | testRuntimeOnly 'cglib:cglib-nodep:3.3.0' 23 | testRuntimeOnly 'org.objenesis:objenesis:3.4' 24 | } 25 | 26 | test { 27 | useJUnitPlatform() 28 | mustRunAfter ':core:check' 29 | 30 | if (System.getProperty('os.name') == 'Linux') { 31 | systemProperty 'java.security.egd', 'file:/dev/./urandom' 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server-integration-test/src/main/groovy/org/hidetake/groovy/ssh/test/server/CommandHelper.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.server 2 | 3 | import groovy.util.logging.Slf4j 4 | import org.apache.sshd.server.Environment 5 | import org.apache.sshd.server.ExitCallback 6 | import org.apache.sshd.server.command.Command 7 | 8 | import static org.hidetake.groovy.ssh.util.Utility.callWithDelegate 9 | 10 | @Slf4j 11 | class CommandHelper { 12 | 13 | static class CommandContext { 14 | InputStream inputStream 15 | OutputStream outputStream 16 | OutputStream errorStream 17 | ExitCallback exitCallback 18 | Environment environment 19 | } 20 | 21 | static command(int status, @DelegatesTo(CommandContext) Closure interaction = {}) { 22 | def context = new CommandContext() 23 | [setInputStream: { InputStream inputStream -> 24 | context.inputStream = inputStream 25 | }, 26 | setOutputStream: { OutputStream outputStream -> 27 | context.outputStream = outputStream 28 | }, 29 | setErrorStream: { OutputStream errorStream -> 30 | context.errorStream = errorStream 31 | }, 32 | setExitCallback: { ExitCallback callback -> 33 | context.exitCallback = callback 34 | }, 35 | start: { Environment env -> 36 | context.environment = env 37 | Thread.start { 38 | log.debug("[ssh-server-mock] Started interaction thread") 39 | try { 40 | callWithDelegate(interaction, context) 41 | context.exitCallback.onExit(status) 42 | } catch (Throwable t) { 43 | log.error("[ssh-server-mock] Error occurred on interaction thread", t) 44 | context.exitCallback.onExit(-1, t.message) 45 | } 46 | log.debug("[ssh-server-mock] Terminated interaction thread") 47 | } 48 | }, 49 | destroy: { -> 50 | }] as Command 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /server-integration-test/src/main/groovy/org/hidetake/groovy/ssh/test/server/FileDivCategory.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.server 2 | 3 | @Category(File) 4 | class FileDivCategory { 5 | 6 | File div(String child) { 7 | new File(this as File, child) 8 | } 9 | 10 | File div(DirectoryType type) { 11 | switch (type) { 12 | case DirectoryType.DIRECTORY: 13 | assert mkdir() 14 | break 15 | 16 | case DirectoryType.DIRECTORIES: 17 | assert mkdirs() 18 | break 19 | 20 | default: 21 | throw new IllegalArgumentException("Unknown directory type: $type") 22 | } 23 | this 24 | } 25 | 26 | static enum DirectoryType { 27 | DIRECTORY, 28 | DIRECTORIES 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /server-integration-test/src/main/groovy/org/hidetake/groovy/ssh/test/server/FilenameUtils.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.server 2 | 3 | class FilenameUtils { 4 | 5 | /** 6 | * Convert Windows path to Unix path for SFTP subsystem of Apache SSHD server. 7 | * This method does nothing on Unix platform. 8 | * 9 | * @param path 10 | * @return 11 | */ 12 | static String toUnixPath(String path) { 13 | if (File.separator == '/') { 14 | path 15 | } else { 16 | path.replace(File.separatorChar, '/' as char).replaceFirst(~/(\w):/, '/$1') 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /server-integration-test/src/main/groovy/org/hidetake/groovy/ssh/test/server/HostKeyFixture.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.server 2 | 3 | import org.apache.sshd.common.keyprovider.ClassLoadableResourceKeyPairProvider 4 | 5 | class HostKeyFixture { 6 | 7 | static publicKey(String keyType) { 8 | HostKeyFixture.getResourceAsStream("/hostkey_${keyType}.pub").text 9 | } 10 | 11 | static publicKeys(List keyTypes) { 12 | keyTypes.collect { keyType -> publicKey(keyType) } 13 | } 14 | 15 | static keyPairProvider(String... keyTypes) { 16 | keyPairProvider(keyTypes.toList()) 17 | } 18 | 19 | static keyPairProvider(List keyTypes) { 20 | def keyPairProvider = new ClassLoadableResourceKeyPairProvider() 21 | keyPairProvider.resourceLoader = HostKeyFixture.classLoader 22 | keyPairProvider.resources = keyTypes.collect { "hostkey_$it".toString() } 23 | keyPairProvider 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /server-integration-test/src/main/groovy/org/hidetake/groovy/ssh/test/server/SshServerMock.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.server 2 | 3 | import groovy.util.logging.Slf4j 4 | import org.apache.sshd.common.keyprovider.KeyPairProvider 5 | import org.apache.sshd.server.SshServer 6 | 7 | import static org.apache.sshd.common.keyprovider.KeyPairProvider.ECDSA_SHA2_NISTP256 8 | import static org.hidetake.groovy.ssh.test.server.HostKeyFixture.keyPairProvider 9 | 10 | /** 11 | * A helper class for server-based integration tests. 12 | * 13 | * @author Hidetake Iwata 14 | */ 15 | @Slf4j 16 | class SshServerMock { 17 | 18 | static SshServer setUpLocalhostServer(KeyPairProvider provider = keyPairProvider(ECDSA_SHA2_NISTP256)) { 19 | SshServer.setUpDefaultServer().with { 20 | host = 'localhost' 21 | port = pickUpFreePort() 22 | keyPairProvider = provider 23 | it 24 | } 25 | } 26 | 27 | static int pickUpFreePort() { 28 | def socket = new ServerSocket(0) 29 | def port = socket.localPort 30 | socket.close() 31 | port 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /server-integration-test/src/main/groovy/org/hidetake/groovy/ssh/test/server/SudoHelper.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.server 2 | 3 | import groovy.transform.Immutable 4 | import groovy.util.logging.Slf4j 5 | 6 | import static org.hidetake.groovy.ssh.util.Utility.callWithDelegate 7 | 8 | @Slf4j 9 | class SudoHelper { 10 | 11 | @Immutable 12 | static class ParsedCommandLine { 13 | final String sudoPath 14 | final String prompt 15 | final String command 16 | 17 | static ParsedCommandLine parse(String commandLine) { 18 | def matcher = commandLine =~ /^(.+?) -S -p '(.+?)' (.+)$/ 19 | assert matcher.matches() 20 | def groups = matcher[0] as List 21 | new ParsedCommandLine(sudoPath: groups[1], prompt: groups[2], command: groups[3]) 22 | } 23 | } 24 | 25 | static sudoCommand(String commandLine, int status, 26 | String expectedSudoPath, String expectedCommand, String expectedPassword, 27 | String lectureMessage = null, 28 | @DelegatesTo(CommandHelper.CommandContext) Closure closure = null) { 29 | CommandHelper.command(status) { 30 | def parsedCommandLine = ParsedCommandLine.parse(commandLine) 31 | assert parsedCommandLine.sudoPath == expectedSudoPath 32 | assert parsedCommandLine.command == expectedCommand 33 | 34 | if (lectureMessage) { 35 | log.debug("[sudo] Sending to standard output: $lectureMessage") 36 | outputStream << lectureMessage << '\n' 37 | } 38 | 39 | log.debug("[sudo] Sending prompt: $parsedCommandLine.prompt") 40 | outputStream << parsedCommandLine.prompt 41 | outputStream.flush() 42 | 43 | log.debug('[sudo] Waiting for password') 44 | def actualPassword = new ByteArrayOutputStream() 45 | for (;;) { 46 | def b = inputStream.read() 47 | if (b == 0x0a || b == 0x0d || b == -1) { 48 | break 49 | } 50 | actualPassword.write(b) 51 | } 52 | 53 | log.debug("[sudo] Got password: $actualPassword") 54 | assert actualPassword.toString() == expectedPassword 55 | 56 | log.debug('[sudo] Sending ACK to the password') 57 | outputStream << '\n' 58 | outputStream.flush() 59 | 60 | if (closure) { 61 | log.debug('[sudo] Starting interaction') 62 | delegate.metaClass.parsedCommandLine = parsedCommandLine 63 | callWithDelegate(closure, delegate) 64 | } 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /server-integration-test/src/main/groovy/org/hidetake/groovy/ssh/test/server/UserKeyFixture.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.server 2 | 3 | class UserKeyFixture { 4 | 5 | static enum KeyType { 6 | ecdsa, 7 | ecdsa_pass 8 | } 9 | 10 | static privateKey(KeyType keyType = KeyType.ecdsa) { 11 | new File(UserKeyFixture.getResource("/id_$keyType").file) 12 | } 13 | 14 | static publicKey(KeyType keyType = KeyType.ecdsa) { 15 | new File(UserKeyFixture.getResource("/id_${keyType}.pub").file) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /server-integration-test/src/test/groovy/org/hidetake/groovy/ssh/test/server/DryRunSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.server 2 | 3 | import org.hidetake.groovy.ssh.Ssh 4 | import org.hidetake.groovy.ssh.core.Service 5 | import spock.lang.Specification 6 | 7 | class DryRunSpec extends Specification { 8 | 9 | Service ssh 10 | 11 | def setup() { 12 | ssh = Ssh.newService() 13 | ssh.settings { 14 | knownHosts = allowAnyHosts 15 | } 16 | ssh.remotes { 17 | testServer { 18 | host = 'localhost' 19 | user = 'user' 20 | dryRun = true 21 | } 22 | } 23 | } 24 | 25 | 26 | def "dry-run shell should work without server"() { 27 | when: 28 | ssh.run { 29 | session(ssh.remotes.testServer) { 30 | shell(interaction: {}) 31 | } 32 | } 33 | 34 | then: 35 | noExceptionThrown() 36 | } 37 | 38 | def "dry-run shell with options should work without server"() { 39 | when: 40 | ssh.run { 41 | session(ssh.remotes.testServer) { 42 | shell(logging: 'none') 43 | } 44 | } 45 | 46 | then: 47 | noExceptionThrown() 48 | } 49 | 50 | def "dry-run command should work without server"() { 51 | when: 52 | ssh.run { 53 | session(ssh.remotes.testServer) { 54 | execute('ls -l') 55 | } 56 | } 57 | 58 | then: 59 | noExceptionThrown() 60 | } 61 | 62 | def "dry-run command with callback should work without server"() { 63 | given: 64 | def callbackExecuted = false 65 | 66 | when: 67 | ssh.run { 68 | session(ssh.remotes.testServer) { 69 | execute('ls -l') { 70 | callbackExecuted = true 71 | } 72 | } 73 | } 74 | 75 | then: 76 | callbackExecuted 77 | } 78 | 79 | def "dry-run command with options should work without server"() { 80 | when: 81 | ssh.run { 82 | session(ssh.remotes.testServer) { 83 | execute('ls -l', pty: true) 84 | } 85 | } 86 | 87 | then: 88 | noExceptionThrown() 89 | } 90 | 91 | def "dry-run command with options and callback should work without server"() { 92 | given: 93 | def callbackExecuted = false 94 | 95 | when: 96 | ssh.run { 97 | session(ssh.remotes.testServer) { 98 | execute('ls -l', pty: true) { 99 | callbackExecuted = true 100 | } 101 | } 102 | } 103 | 104 | then: 105 | callbackExecuted 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /server-integration-test/src/test/groovy/org/hidetake/groovy/ssh/test/server/ExtensionSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.server 2 | 3 | import org.apache.sshd.server.SshServer 4 | import org.apache.sshd.server.auth.password.PasswordAuthenticator 5 | import org.apache.sshd.server.command.CommandFactory 6 | import org.hidetake.groovy.ssh.Ssh 7 | import org.hidetake.groovy.ssh.core.Service 8 | import spock.lang.Shared 9 | import spock.lang.Specification 10 | import spock.util.concurrent.PollingConditions 11 | 12 | import static org.hidetake.groovy.ssh.test.server.CommandHelper.command 13 | 14 | class ExtensionSpec extends Specification { 15 | 16 | @Shared 17 | SshServer server 18 | 19 | Service ssh 20 | 21 | def setupSpec() { 22 | server = SshServerMock.setUpLocalhostServer() 23 | server.passwordAuthenticator = Mock(PasswordAuthenticator) { 24 | authenticate('someuser', 'somepassword', _) >> true 25 | } 26 | server.start() 27 | } 28 | 29 | def cleanupSpec() { 30 | new PollingConditions().eventually { 31 | assert server.activeSessions.empty 32 | } 33 | server.stop() 34 | } 35 | 36 | def setup() { 37 | server.commandFactory = Mock(CommandFactory) 38 | 39 | ssh = Ssh.newService() 40 | ssh.settings { 41 | knownHosts = allowAnyHosts 42 | } 43 | ssh.remotes { 44 | testServer { 45 | host = server.host 46 | port = server.port 47 | user = 'someuser' 48 | password = 'somepassword' 49 | } 50 | } 51 | } 52 | 53 | def "adding map to ssh.settings.extensions should extends DSL"() { 54 | given: 55 | ssh.settings { 56 | extensions.add restartAppServer: { 57 | execute 'sudo service tomcat restart' 58 | } 59 | } 60 | 61 | when: 62 | ssh.run { 63 | session(ssh.remotes.testServer) { 64 | restartAppServer() 65 | } 66 | } 67 | 68 | then: 1 * server.commandFactory.createCommand('sudo service tomcat restart') >> command(0) 69 | } 70 | 71 | def "adding map to settings.extensions should extends DSL"() { 72 | when: 73 | ssh.run { 74 | settings { 75 | extensions.add restartAppServer: { 76 | execute 'sudo service tomcat restart' 77 | } 78 | } 79 | session(ssh.remotes.testServer) { 80 | restartAppServer() 81 | } 82 | } 83 | 84 | then: 1 * server.commandFactory.createCommand('sudo service tomcat restart') >> command(0) 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /server-integration-test/src/test/groovy/org/hidetake/groovy/ssh/test/server/RetrySpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.server 2 | 3 | import com.jcraft.jsch.JSchException 4 | import org.apache.sshd.server.SshServer 5 | import org.apache.sshd.server.auth.password.PasswordAuthenticator 6 | import org.hidetake.groovy.ssh.Ssh 7 | import org.hidetake.groovy.ssh.core.Service 8 | import spock.lang.Shared 9 | import spock.lang.Specification 10 | import spock.lang.Timeout 11 | 12 | @Timeout(10) 13 | class RetrySpec extends Specification { 14 | 15 | @Shared 16 | SshServer server 17 | 18 | Service ssh 19 | 20 | def setupSpec() { 21 | server = SshServerMock.setUpLocalhostServer() 22 | } 23 | 24 | def setup() { 25 | server.passwordAuthenticator = Mock(PasswordAuthenticator) 26 | 27 | ssh = Ssh.newService() 28 | ssh.settings { 29 | knownHosts = allowAnyHosts 30 | } 31 | ssh.remotes { 32 | testServer { 33 | host = server.host 34 | port = server.port 35 | user = 'someuser' 36 | password = 'somepassword' 37 | } 38 | } 39 | } 40 | 41 | 42 | def 'should retry but fail'() { 43 | given: 44 | ssh.settings { 45 | retryWaitSec = 1 46 | retryCount = 1 47 | } 48 | 49 | when: 50 | ssh.run { 51 | session(ssh.remotes.testServer) {} 52 | } 53 | 54 | then: 'connection refused' 55 | JSchException e = thrown() 56 | e.cause instanceof ConnectException 57 | } 58 | 59 | def 'should retry and success'() { 60 | given: 61 | ssh.settings { 62 | retryWaitSec = 2 63 | retryCount = 1 64 | } 65 | 66 | and: 'start SSH server after 1 second' 67 | def thread = Thread.start { 68 | Thread.sleep(1000L) 69 | server.start() 70 | } 71 | 72 | when: 73 | ssh.run { 74 | session(ssh.remotes.testServer) {} 75 | } 76 | 77 | then: 78 | (1.._) * server.passwordAuthenticator.authenticate('someuser', 'somepassword', _) >> true 79 | 80 | cleanup: 81 | thread.join() 82 | server.stop() 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /server-integration-test/src/test/groovy/org/hidetake/groovy/ssh/test/server/ScpSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.server 2 | 3 | import org.apache.sshd.server.scp.ScpCommandFactory 4 | import org.hidetake.groovy.ssh.session.transfer.FileTransferMethod 5 | import spock.lang.Timeout 6 | 7 | @Timeout(10) 8 | class ScpSpec extends AbstractFileTransferSpec { 9 | 10 | def setupSpec() { 11 | server.commandFactory = new ScpCommandFactory() 12 | server.start() 13 | } 14 | 15 | def setup() { 16 | ssh.settings { 17 | fileTransfer = FileTransferMethod.scp 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /server-integration-test/src/test/groovy/org/hidetake/groovy/ssh/test/server/SftpSpec.groovy: -------------------------------------------------------------------------------- 1 | package org.hidetake.groovy.ssh.test.server 2 | 3 | import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory 4 | import org.hidetake.groovy.ssh.operation.SftpException 5 | 6 | import static org.hidetake.groovy.ssh.test.server.FilenameUtils.toUnixPath 7 | 8 | class SftpSpec extends AbstractFileTransferSpec { 9 | 10 | def setupSpec() { 11 | server.subsystemFactories = [new SftpSubsystemFactory()] 12 | server.start() 13 | } 14 | 15 | 16 | //FIXME: should be in AbstractFileTransferSpec but put here due to bug of Apache SSHD 17 | def "put(dir) should throw IOException if destination does not exist"() { 18 | given: 19 | def sourceDir = temporaryFolder.newFolder('source') 20 | sourceDir / 'file1' << 'Source Content 1' 21 | def destinationDir = temporaryFolder.newFolder('destination') / 'dir1' 22 | assert !destinationDir.exists() 23 | 24 | when: 25 | ssh.run { 26 | session(ssh.remotes.testServer) { 27 | put from: sourceDir, into: toUnixPath(destinationDir.path) 28 | } 29 | } 30 | 31 | then: 32 | IOException e = thrown() 33 | e.message.contains(toUnixPath(destinationDir.path)) 34 | } 35 | 36 | 37 | def "sftp.mkdir() should fail if directory already exists"() { 38 | given: 39 | def folder = temporaryFolder.newFolder() 40 | 41 | when: 42 | ssh.run { 43 | session(ssh.remotes.testServer) { 44 | sftp { 45 | mkdir folder.path 46 | } 47 | } 48 | } 49 | 50 | then: 51 | SftpException e = thrown() 52 | e.message.contains('SFTP MKDIR') 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /server-integration-test/src/test/resources/hostkey_ecdsa-sha2-nistp256: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS 3 | 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTb2Qpge+BrGw6SKfi2lTzubUPS5FYg 4 | OGJBOV3IgNGJOP4q+lQZV4KQCq+XgtDsL3OxAOFya/sa3dNI90Sh5ZXoAAAAsOBC60DgQu 5 | tAAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNvZCmB74GsbDpIp 6 | +LaVPO5tQ9LkViA4YkE5XciA0Yk4/ir6VBlXgpAKr5eC0Owvc7EA4XJr+xrd00j3RKHlle 7 | gAAAAgQhtKq9f2GGDeivbuZ3tPKWtzabtWWkBBMCP79B1WIhcAAAAVaGlkZXRha2VAcmFi 8 | Yml0LmxvY2FsAQID 9 | -----END OPENSSH PRIVATE KEY----- 10 | -------------------------------------------------------------------------------- /server-integration-test/src/test/resources/hostkey_ecdsa-sha2-nistp256.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNvZCmB74GsbDpIp+LaVPO5tQ9LkViA4YkE5XciA0Yk4/ir6VBlXgpAKr5eC0Owvc7EA4XJr+xrd00j3RKHlleg= 2 | -------------------------------------------------------------------------------- /server-integration-test/src/test/resources/hostkey_ecdsa-sha2-nistp256_another.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCg59Lu5tQaXDLtoGRRnEAG7bjV0yykN+PLB3iFVMBmnW24DFBVow955yBhbS7WeICKLG4pGk03XD+YFA0stxds= 2 | -------------------------------------------------------------------------------- /server-integration-test/src/test/resources/hostkey_ssh-dss: -------------------------------------------------------------------------------- 1 | -----BEGIN DSA PRIVATE KEY----- 2 | MIIBugIBAAKBgQDr7tBpE/LrkghfUbe4issLb3tcLTZlpCDxC2Vy1f8E4vkpua8R 3 | eSBSJxuXSzaLnObhGc4s5y38/mR+BS+OLCW2vH57OTXShbINlGF8Y5okjL7tc0eo 4 | owHzrDyJ1U0UeBBCkmmCPJjZna4xNGnakKXkBRRLheHMCpd+X/3HExwO+wIVAIRI 5 | nG3eitk3sM4zhEihAvlsX0R/AoGAJC3mvwWmOmgM+djKPHpdSPx+gRFFV6nIGeay 6 | dJnJZfDpZ9UEVhFGBVIIj7TVHMA8WeXLdEaIZvYfy1sECWTWhJE0aXYzHVYx/N5C 7 | GixHH0oYSPGtmbfRbTJ/itoHHbBlZAMN2mhdiRD/hgmUndmUiNjOfjAzGfSPoTSo 8 | 1eFI3QICgYBRt25Cp4EQ0zX48BLKhRylZ7IgcBi7CvzHNUgvRfQJ78b/7CG10pYn 9 | PjRs4OqM0YkccklGrOXafFwTa+iK8PsBU/4IAy4XnJ5XStE7FrGEfVhlyOr+bi6C 10 | pjXk3opqJjt+5ImXSE6+bZ8JMt86B626Na+wba7QqU6NR/6viRxXRwIUUKFC8L/H 11 | id4VH959BWtteLkCxng= 12 | -----END DSA PRIVATE KEY----- -------------------------------------------------------------------------------- /server-integration-test/src/test/resources/hostkey_ssh-dss.pub: -------------------------------------------------------------------------------- 1 | ssh-dss AAAAB3NzaC1kc3MAAACBAOvu0GkT8uuSCF9Rt7iKywtve1wtNmWkIPELZXLV/wTi+Sm5rxF5IFInG5dLNouc5uEZziznLfz+ZH4FL44sJba8fns5NdKFsg2UYXxjmiSMvu1zR6ijAfOsPInVTRR4EEKSaYI8mNmdrjE0adqQpeQFFEuF4cwKl35f/ccTHA77AAAAFQCESJxt3orZN7DOM4RIoQL5bF9EfwAAAIAkLea/BaY6aAz52Mo8el1I/H6BEUVXqcgZ5rJ0mcll8Oln1QRWEUYFUgiPtNUcwDxZ5ct0Rohm9h/LWwQJZNaEkTRpdjMdVjH83kIaLEcfShhI8a2Zt9FtMn+K2gcdsGVkAw3aaF2JEP+GCZSd2ZSI2M5+MDMZ9I+hNKjV4UjdAgAAAIBRt25Cp4EQ0zX48BLKhRylZ7IgcBi7CvzHNUgvRfQJ78b/7CG10pYnPjRs4OqM0YkccklGrOXafFwTa+iK8PsBU/4IAy4XnJ5XStE7FrGEfVhlyOr+bi6CpjXk3opqJjt+5ImXSE6+bZ8JMt86B626Na+wba7QqU6NR/6viRxXRw== 2 | -------------------------------------------------------------------------------- /server-integration-test/src/test/resources/hostkey_ssh-rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKQIBAAKCAgEA+LMta2lFZvgsoASVoH/5Pw8B01GsxJS8lT+cLeSvJo5xY1ET 3 | U1ezhbip3FvO5GPq6zqd27dwSYnywgG+o7rSD0Og4ncgY+a51Dy+fi8wbxY4v4GN 4 | EYZFO572d7qvSVusSI/u+oFbyxQc5NLgYcM9erJB7IRB0jVQjSYO9jsPLEcFyHBd 5 | GVAhCR1A1hm1aYwYhVN/7ffA937v87cYzD7v2pwZiMvc/oKPTp3RYUmrZdLEgIo1 6 | AFu7FBNQdSegiSCzIO+1/zumxR6TxEp4wvG78EltwGcy86Q8gk1Re1o5BLGjHIsF 7 | zwlcHCMn/TgCy7P9Kxs6wJhLlEII51Moeyx2GVY/vr7E5xNwTc23woSPTngTclU+ 8 | qSKb9l+WdYTTshWIGyRcTazYEA+dFHCGbMKnRFsI2JjPfcmQ0TmDTbBAT3YkB3sS 9 | SNVa+MSJIhYDrL23QMfJcAfz81EKBIvQLruyX99/Z7TplkhSkiQz9lvOKwcOr1zJ 10 | hIazxwsTnjswkl4RS2yfIaUltmJJImCSWBgn8JDCXYeKdNSYdZnlX8e2pIxZ5f5b 11 | uXAwvQrMs+yslMEgsOpLvXRPGVaAdkGUtq3zmO7GGduKwSi/Rf/ht0nqE5RJcchT 12 | IJpP+2xDy2Z3RfFXWciAOupkeNZWOsE5TCgBVZFfm9OfDGOV38L0ShCgwk8CAwEA 13 | AQKCAgEAxMxkoJ6JANZZ2bZHAN7DyRaDB0mWQWjBYgkX+WkBAK2vJDw/4q9/q81Y 14 | /LrZmPlIKCQWTot2G3tB0iu3Q7DOEK4fXZWO1/74Be6bfxawaPdYqJJHoxpxJqC/ 15 | wDbGBnK4fiMHpMtAbA7aXmhQjowE1lbAu/xcaY8u0sLPDCHn+82n0di4kxNJUQEJ 16 | EWL+nyrxLG/Kx/BJjo+wKVc2FEmpW4Ay5IENu4htBnTz/txg+Q4z2NOE5WexPk2a 17 | WVqmOlzZ4rJNfk8hxNJRc/7KWNkZMyen5ZzEQPAxwZqVY9sdS42V2TxZuF9buBhJ 18 | yKLN84vWRoohj83z4LnwebzgR7gDKrIpbGgYf1WKf3TFkEeX/N9G2iwDU4Qes1kc 19 | 47NF32v8tApj0P9hldNJO64TrUrZLvc6pI85XvY4Ljk37c4WQhDfIaLbPVkukwAo 20 | JnaGkHZIH4ORWRbE03eNABALjXDi1uL0oihRaNhj6HDjLRhX67/A0ehEvwhOtlNV 21 | WzMRYUaGEOjqASpLuSr+hTAGwxe/GEyxBhacaCFHBVyYcN0LQmVyHR2rYTHeFYsj 22 | fGlxcgIzX9/ldZsz8jHfB/f6369tqki550lpA5QM+gnk9JM1RAmqWD/LHB4wIPXw 23 | RkjCPpLqfA5EhOl+zhu7+RJkqQ8g2N+zovgQYFLavxHAoA2uL+ECggEBAP/NB6o9 24 | 3WGtR1tn70f1QMv4atMRJp6JFYxFOJDiNclgY76klAG9mEID0ul2rEmfTLmEuC/T 25 | bynOMyEP9eg+ui9/vXp7OU0km8Bl9EfvXqGTFegC5EYbfcCM9V+IKl9gzvob1MDV 26 | OJY8rJwFzZbYn4rWHmmRmA2whaCF6GqPVU5DyV6Vf4/RjS6qhkD38OCg5wv8dFN2 27 | ETTBdBHajQxHTEMpSHAeAhyNHa6POFevLddmZTRcWUdp2MhXBOK63Lut8N1iWPZX 28 | b0c4pnVE4g7I9kN9Ee2ID0Naq91g0cIpo7/gYIiCXiM/NKoa6UinLffFLo+4PRDJ 29 | 3Zgm44HYVp701z0CggEBAPjku4kAZXYU+9O7HeiLQwIoXr/lC9kvffX4Lpjek2kF 30 | u/uWJCjeiNbTDlnJ0QbZwQNOt1XSbEJUMt7m0bgmd4YZCBoTwJZt8ijyeUJLy31O 31 | S6IlTRqw0azXOA+IqAdVLjgV2bJeZ+AbxtLmKFFOEDdEWJdUZm9tdBKwExcoZQHd 32 | n4UlzLZZQgcsjyNa4ujuAnNpCMc69VFgJR1ZPrZ6bMIq+bBl9oabE1vhMdP9DJ5k 33 | VV6sXiZVFWcKQXQBDDoKSw4STaoFlZ9WGSjMbVRa36zwTT5umaLg29HbbmlSKJhL 34 | PhQ2jkKOX5YyA59mzI/LKzWkHmRu0gO2dFgD84gIOHsCggEAKSWj7ACTkdi7t7pa 35 | RSrwR18oX9dMbQgEDVI9LSEWIdR9dvcmT7ldupC+5osOX7FeOCFfN2I9aW4fz1uv 36 | 5LHiZS2ZHxCfQQd4flRoHQImKLtuNKnyakyVx9lkxv8yQbFi2dyMNpCVz6Tm5xoP 37 | 3gnub/jNX5Hp8ZnJhO7FWg7AXByPUcWehVXh7fXELsMzXg7D26u6UoqVbfUIZK7R 38 | LX5yiAMmAdLx6AHTGnczSveE2jdAZFHOkqw2ENPel1ITm6T5L8NYKBgccEpujEKO 39 | Um6FHooJzjmqqtz09GFUnHTyQDUpkgvWdLkAEbTOfb9Wp5pySPAL8p1mJclro0FX 40 | fa5IyQKCAQEA68uG/5wog14N1eGHzC9jJEAqw5GzT+b2ZnjhbGIxNWYBWbA8QZvR 41 | 9/OrvNS9+seBXCjO97eKaKxsJVB3jwaiZaCBVHMyGVHAAZr11iEIf4YVkV/VVFv/ 42 | yMd8jPridU/1oZzRyXI25tJp4pO5zo0uP624YGXNRfNj6/BzpZTXXJUh83qEHp0h 43 | OLq2QMByCEq0d7IiZt0sXBPCFbPe/fGt56XfN4bhr1RrWrhWBIfP1J8pYa/a4mXp 44 | CN1mdQgpFTN7N7GPcmwn+/QNkeFaEnilP6nJBESzuONeXcqNJ6p2WxGtlmi5DcTk 45 | Od7YH8O8ON456W+6e1uiC3JndpsHtPBU0QKCAQBGI9tO0oB2dYicxOqP90pex4Qo 46 | JIm4CG3GnYKfNzcS+lNW8biCa/hJI0by019PTt0Jc8R3qpdnVUdVTcPY/0ebmr7S 47 | 8KZMQzqGeeIh9vSTIyVU78ZPF9AARw5nl5oNTWILaJTrntEFKFSfa/n0WEYAkwRU 48 | 3xwC3NEDtCWL+ffHY7GgoBn9i2udzn79SWMNG4cOGViJ+wZT3qyWZ8la00Gu2RM7 49 | ROuAy6LkAlV/sbfeAFLEVI3gMWDvrbmG1EPtEFxfe/3ueUSnY0H4zKEMHaq7W8Wo 50 | UqAULdgP8zTckphmpWZzI8GoIJWIumtn2w8o4oJ0MwZhFJbqgxUIrU8GAoPD 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /server-integration-test/src/test/resources/hostkey_ssh-rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQD4sy1raUVm+CygBJWgf/k/DwHTUazElLyVP5wt5K8mjnFjURNTV7OFuKncW87kY+rrOp3bt3BJifLCAb6jutIPQ6DidyBj5rnUPL5+LzBvFji/gY0RhkU7nvZ3uq9JW6xIj+76gVvLFBzk0uBhwz16skHshEHSNVCNJg72Ow8sRwXIcF0ZUCEJHUDWGbVpjBiFU3/t98D3fu/ztxjMPu/anBmIy9z+go9OndFhSatl0sSAijUAW7sUE1B1J6CJILMg77X/O6bFHpPESnjC8bvwSW3AZzLzpDyCTVF7WjkEsaMciwXPCVwcIyf9OALLs/0rGzrAmEuUQgjnUyh7LHYZVj++vsTnE3BNzbfChI9OeBNyVT6pIpv2X5Z1hNOyFYgbJFxNrNgQD50UcIZswqdEWwjYmM99yZDROYNNsEBPdiQHexJI1Vr4xIkiFgOsvbdAx8lwB/PzUQoEi9Auu7Jf339ntOmWSFKSJDP2W84rBw6vXMmEhrPHCxOeOzCSXhFLbJ8hpSW2YkkiYJJYGCfwkMJdh4p01Jh1meVfx7akjFnl/lu5cDC9Csyz7KyUwSCw6ku9dE8ZVoB2QZS2rfOY7sYZ24rBKL9F/+G3SeoTlElxyFMgmk/7bEPLZndF8VdZyIA66mR41lY6wTlMKAFVkV+b058MY5XfwvRKEKDCTw== 2 | -------------------------------------------------------------------------------- /server-integration-test/src/test/resources/id_ecdsa: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MIIBaAIBAQQg2d776WhdQFfLXan9bhU/bcSaf6HZAv27Js851vDZMImggfowgfcC 3 | AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA//////////////// 4 | MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr 5 | vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE 6 | axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W 7 | K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8 8 | YyVRAgEBoUQDQgAESZT7K4plmvEATrWFhPWybRw4ptSJ7l3rXNc/Dh6GBaLDJli0 9 | SYf6kOaH8xl/EHpbxBTE6P8KVJga1uesO4WbVg== 10 | -----END EC PRIVATE KEY----- 11 | -------------------------------------------------------------------------------- /server-integration-test/src/test/resources/id_ecdsa.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmU+yuKZZrxAE61hYT1sm0cOKbUie5d61zXPw4ehgWiwyZYtEmH+pDmh/MZfxB6W8QUxOj/ClSYGtbnrDuFm1Y= 2 | -------------------------------------------------------------------------------- /server-integration-test/src/test/resources/id_ecdsa_pass: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDXw8+TMm 3 | PD/3Ucw/UBXMWeAAAAEAAAAAEAAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlz 4 | dHAyNTYAAABBBOrnnc2zLhs1gnsU5EmpRoZjhVWwPZb/UG04SfxgE6o1aujA5i89L4fHSr 5 | R6Qwkmede88PQb9wzo7pOy7pOhO7MAAADAHSwb0T4JGfAZH2nPL3YV052PfBDMdAkS80qg 6 | 0QMrejOQTMZKz1rGz8BIgqlXXLdxlO/fmUqBbE0L1D5zYfh1aeC3Iqyuuf/FxBFwW4waCL 7 | qidfrGPWS4ASg3huzfuOxgCxJWObk6cYZEXzVrxKCbRxHx8W+Uvr0lsS/pQ5KAuRg+FuDA 8 | 5XZbU+n5tS6U8tcBWbce4GAOMbgTzYZisInJhM57v6pdK++UWK8s2Mj0xPHBpNtd+9Evc5 9 | AYcUruE4iL 10 | -----END OPENSSH PRIVATE KEY----- 11 | -------------------------------------------------------------------------------- /server-integration-test/src/test/resources/id_ecdsa_pass.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOrnnc2zLhs1gnsU5EmpRoZjhVWwPZb/UG04SfxgE6o1aujA5i89L4fHSrR6Qwkmede88PQb9wzo7pOy7pOhO7M= codespace@codespaces-7f1977 2 | -------------------------------------------------------------------------------- /server-integration-test/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'groovy-ssh' 2 | 3 | include 'core' 4 | include 'cli' 5 | include 'server-integration-test' 6 | include 'os-integration-test' 7 | include 'docs' 8 | --------------------------------------------------------------------------------