[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\ncustom: ['https://www.paypal.me/appleboy46']\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: /\n    schedule:\n      interval: weekly\n  - package-ecosystem: gomod\n    directory: /\n    schedule:\n      interval: weekly\n"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [master]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [master]\n  schedule:\n    - cron: \"41 23 * * 6\"\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [\"go\"]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]\n        # Learn more about CodeQL language support at https://git.io/codeql-language-support\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      # Initializes the CodeQL tools for scanning.\n      - name: Initialize CodeQL\n        uses: github/codeql-action/init@v4\n        with:\n          languages: ${{ matrix.language }}\n          # If you wish to specify custom queries, you can do so here or in a config file.\n          # By default, queries listed here will override any specified in a config file.\n          # Prefix the list here with \"+\" to use these queries and those in the config file.\n          # queries: ./path/to/local/query, your-org/your-repo/queries@main\n\n      - name: Perform CodeQL Analysis\n        uses: github/codeql-action/analyze@v4\n"
  },
  {
    "path": ".github/workflows/goreleaser.yml",
    "content": "name: Goreleaser\n\non:\n  push:\n    tags:\n      - \"*\"\n\npermissions:\n  contents: write\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0 # all history for all branches and tags\n\n      - name: Setup go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: go.mod\n          check-latest: true\n\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@v7\n        with:\n          # either 'goreleaser' (default) or 'goreleaser-pro'\n          distribution: goreleaser\n          # 'latest', 'nightly', or a semver\n          version: '~> v2'\n          args: release --clean\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/testing.yml",
    "content": "name: Lint and Testing\n\non:\n  push:\n  pull_request:\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0 # all history for all branches and tags\n\n      - name: Setup go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: go.mod\n          check-latest: true\n      - name: Setup golangci-lint\n        uses: golangci/golangci-lint-action@v9\n        with:\n          version: latest\n          args: --verbose\n\n  testing:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        go-version: ['1.25', '1.26']\n    container: golang:${{ matrix.go-version }}-alpine\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0 # all history for all branches and tags\n\n      - name: setup sshd server\n        run: |\n          apk add git make curl perl bash build-base zlib-dev ucl-dev sudo\n          make ssh-server\n\n      - name: testing\n        run: |\n          make test\n\n      - name: Upload coverage to Codecov\n        uses: codecov/codecov-action@v6\n"
  },
  {
    "path": ".github/workflows/trivy.yml",
    "content": "name: Trivy Security Scan\n\non:\n  pull_request:\n    branches:\n      - master\n  push:\n    branches:\n      - master\n  schedule:\n    - cron: \"0 1 * * *\" # Run daily at 1:00 AM UTC\n\njobs:\n  trivy-scan:\n    name: Trivy Vulnerability Scan\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      security-events: write\n      actions: read\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Run Trivy vulnerability scanner in fs mode\n        uses: aquasecurity/trivy-action@0.35.0\n        with:\n          scan-type: \"fs\"\n          scan-ref: \".\"\n          format: \"sarif\"\n          output: \"trivy-results.sarif\"\n          severity: \"CRITICAL,HIGH,MEDIUM\"\n          exit-code: \"0\" # Don't fail on vulnerabilities, only record them\n\n      - name: Upload Trivy results to GitHub Security tab\n        uses: github/codeql-action/upload-sarif@v4\n        if: always()\n        with:\n          sarif_file: \"trivy-results.sarif\"\n\n      - name: Run Trivy vulnerability scanner in table format\n        uses: aquasecurity/trivy-action@0.35.0\n        with:\n          scan-type: \"fs\"\n          scan-ref: \".\"\n          format: \"table\"\n          severity: \"CRITICAL,HIGH\"\n          exit-code: \"1\" # Fail only if CRITICAL/HIGH vulnerabilities found\n"
  },
  {
    "path": ".gitignore",
    "content": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# Architecture specific extensions/prefixes\n*.[568vq]\n[568vq].out\n\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n\n_testmain.go\n\n*.exe\n*.test\n*.prof\n\ncoverage.txt\n"
  },
  {
    "path": ".goreleaser.yaml",
    "content": "builds:\n  - # If true, skip the build.\n    # Useful for library projects.\n    # Default is false\n    skip: true\n\nchangelog:\n  # Set it to true if you wish to skip the changelog generation.\n  # This may result in an empty release notes on GitHub/GitLab/Gitea.\n  disable: false\n\n  # Changelog generation implementation to use.\n  #\n  # Valid options are:\n  # - `git`: uses `git log`;\n  # - `github`: uses the compare GitHub API, appending the author login to the changelog.\n  # - `gitlab`: uses the compare GitLab API, appending the author name and email to the changelog.\n  # - `github-native`: uses the GitHub release notes generation API, disables the groups feature.\n  #\n  # Defaults to `git`.\n  use: github\n\n  # Sorts the changelog by the commit's messages.\n  # Could either be asc, desc or empty\n  # Default is empty\n  sort: asc\n\n  # Group commits messages by given regex and title.\n  # Order value defines the order of the groups.\n  # Proving no regex means all commits will be grouped under the default group.\n  # Groups are disabled when using github-native, as it already groups things by itself.\n  #\n  # Default is no groups.\n  groups:\n    - title: Features\n      regexp: \"^.*feat[(\\\\w)]*:+.*$\"\n      order: 0\n    - title: \"Bug fixes\"\n      regexp: \"^.*fix[(\\\\w)]*:+.*$\"\n      order: 1\n    - title: \"Enhancements\"\n      regexp: \"^.*chore[(\\\\w)]*:+.*$\"\n      order: 2\n    - title: \"Refactor\"\n      regexp: \"^.*refactor[(\\\\w)]*:+.*$\"\n      order: 3\n    - title: \"Build process updates\"\n      regexp: ^.*?(build|ci)(\\(.+\\))??!?:.+$\n      order: 4\n    - title: \"Documentation updates\"\n      regexp: ^.*?docs?(\\(.+\\))??!?:.+$\n      order: 4\n    - title: Others\n      order: 999\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\neasyssh-proxy is a Go library that provides a simple SSH client implementation with support for SSH tunneling/proxy connections. It's forked from the original easyssh project with additional features for proxy connections, timeout handling, and secure key management.\n\n## Core Architecture\n\n### Main Types\n\n- **MakeConfig**: Primary configuration struct containing SSH connection parameters (user, server, keys, timeouts, proxy settings)\n- **DefaultConfig**: Configuration struct used for SSH proxy/jumphost connections\n- Both structs share similar fields but MakeConfig includes additional proxy capabilities\n\n### Key Methods\n\n- `Connect()`: Establishes SSH session and client connection\n- `Run()`: Executes single command and returns output\n- `Stream()`: Executes command with real-time streaming output via channels\n- `Scp()`: Copies files to remote server\n- `WriteFile()`: Writes content from io.Reader to remote file\n\n### Authentication Support\n\n- Password authentication\n- Private key files (with optional passphrase)\n- Raw private key content (embedded in code)\n- SSH agent integration\n- Custom cipher and key exchange algorithms\n\n### Proxy/Jumphost Architecture\n\nThe library supports SSH proxy connections where traffic is tunneled through an intermediate server:\n\n```text\nClient -> Jumphost -> Target Server\n```\n\nThe `Proxy` field in MakeConfig uses DefaultConfig to define the jumphost connection parameters.\n\n## Development Commands\n\n### Testing\n\n```bash\nmake test                    # Run all tests with coverage\ngo test -v ./...            # Run tests verbose\n```\n\n### Code Quality\n\n```bash\nmake fmt                     # Format code using gofumpt\nmake vet                     # Run go vet\n```\n\n### SSH Test Server Setup\n\n```bash\nmake ssh-server             # Setup local SSH test server (Alpine Linux)\n```\n\n### Linting\n\nThe project uses golangci-lint via GitHub Actions. No local lint command is defined in the Makefile.\n\n## Testing Infrastructure\n\n### Test Environment\n\n- Uses Alpine Linux container with SSH server setup\n- Creates test users: `drone-scp` and `root`\n- SSH keys located in `tests/.ssh/` directory\n- Test files in `tests/` include sample data and configuration\n\n### CI/CD\n\n- GitHub Actions workflow in `.github/workflows/testing.yml`\n- Runs tests in Go 1.25 and 1.26 Alpine containers\n- Includes golangci-lint for code quality\n- Codecov integration for coverage reporting\n\n## Code Patterns\n\n### Configuration Pattern\n\n```go\nssh := &easyssh.MakeConfig{\n    User:    \"username\",\n    Server:  \"hostname\",\n    KeyPath: \"/path/to/key\",\n    Port:    \"22\",\n    Timeout: 60 * time.Second,\n}\n```\n\n### Proxy Configuration\n\n```go\nssh := &easyssh.MakeConfig{\n    // ... main server config\n    Proxy: easyssh.DefaultConfig{\n        // ... jumphost config\n    },\n}\n```\n\n### Error Handling\n\nFunctions return multiple values following Go conventions:\n\n- `Run()`: (stdout, stderr, isTimeout, error)\n- `Stream()`: (stdoutChan, stderrChan, doneChan, errChan, error)\n\n## Example Usage\n\nComprehensive examples are available in `_examples/` directory:\n\n- `ssh/`: Basic command execution\n- `scp/`: File copying\n- `proxy/`: SSH tunneling through jumphost\n- `stream/`: Real-time command output streaming\n- `writeFile/`: Writing content to remote files\n\n## Dependencies\n\n- `golang.org/x/crypto/ssh`: Core SSH protocol implementation\n- `github.com/ScaleFT/sshkeys`: Enhanced private key parsing with passphrase support\n- `github.com/stretchr/testify`: Testing framework\n\n## Security Considerations\n\n- Supports both secure and insecure cipher configurations\n- `UseInsecureCipher` flag enables legacy/weak ciphers when needed\n- Fingerprint verification available for enhanced security\n- Private keys can be embedded as strings or loaded from files\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 Bo-Yi Wu\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "GOFMT ?= gofumpt -l -s\nGO ?= go\nPACKAGES ?= $(shell $(GO) list ./...)\nSOURCES ?= $(shell find . -name \"*.go\" -type f)\n\nall: lint\n\nfmt:\n\t@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \\\n\t\t$(GO) install mvdan.cc/gofumpt; \\\n\tfi\n\t$(GOFMT) -w $(SOURCES)\n\nvet:\n\t$(GO) vet $(PACKAGES)\n\ntest:\n\t@$(GO) test -v -cover -coverprofile coverage.txt $(PACKAGES) && echo \"\\n==>\\033[32m Ok\\033[m\\n\" || exit 1\n\nclean:\n\tgo clean -x -i ./...\n\trm -rf coverage.txt $(EXECUTABLE) $(DIST) vendor\n\nssh-server:\n\tadduser -h /home/drone-scp -s /bin/sh -D -S drone-scp\n\techo drone-scp:1234 | chpasswd\n\tmkdir -p /home/drone-scp/.ssh\n\tchmod 700 /home/drone-scp/.ssh\n\tcat tests/.ssh/id_rsa.pub >> /home/drone-scp/.ssh/authorized_keys\n\tcat tests/.ssh/test.pub >> /home/drone-scp/.ssh/authorized_keys\n\tchmod 600 /home/drone-scp/.ssh/authorized_keys\n\tchown -R drone-scp /home/drone-scp/.ssh\n\t# add public key to root user\n\tmkdir -p /root/.ssh\n\tchmod 700 /root/.ssh\n\tcat tests/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys\n\tcat tests/.ssh/test.pub >> /root/.ssh/authorized_keys\n\tchmod 600 /root/.ssh/authorized_keys\n\t# Append the following entry to run ALL command without a password for a user named drone-scp:\n\tcat tests/sudoers >> /etc/sudoers.d/sudoers\n\t# install ssh and start server\n\tapk add --update openssh openrc\n\trm -rf /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_dsa_key\n\tsed -i 's/^#PubkeyAuthentication yes/PubkeyAuthentication yes/g' /etc/ssh/sshd_config\n\tsed -i 's/AllowTcpForwarding no/AllowTcpForwarding yes/g' /etc/ssh/sshd_config\n\t./tests/entrypoint.sh /usr/sbin/sshd -D &\n"
  },
  {
    "path": "README.md",
    "content": "# easyssh-proxy\n\n[![GoDoc](https://godoc.org/github.com/appleboy/easyssh-proxy?status.svg)](https://pkg.go.dev/github.com/appleboy/easyssh-proxy)\n[![Lint and Testing](https://github.com/appleboy/easyssh-proxy/actions/workflows/testing.yml/badge.svg)](https://github.com/appleboy/easyssh-proxy/actions/workflows/testing.yml)\n[![Trivy Security Scan](https://github.com/appleboy/easyssh-proxy/actions/workflows/trivy.yml/badge.svg)](https://github.com/appleboy/easyssh-proxy/actions/workflows/trivy.yml)\n[![codecov](https://codecov.io/gh/appleboy/easyssh-proxy/branch/master/graph/badge.svg)](https://codecov.io/gh/appleboy/easyssh-proxy)\n[![Go Report Card](https://goreportcard.com/badge/github.com/appleboy/easyssh-proxy)](https://goreportcard.com/report/github.com/appleboy/easyssh-proxy)\n[![Sourcegraph](https://sourcegraph.com/github.com/appleboy/easyssh-proxy/-/badge.svg)](https://sourcegraph.com/github.com/appleboy/easyssh-proxy?badge)\n\n[繁體中文](./README.zh-tw.md)\n\neasyssh-proxy provides a simple implementation of some SSH protocol features in Go.\n\n## Feature\n\nThis project is forked from [easyssh](https://github.com/hypersleep/easyssh) but add some features as the following.\n\n- [x] Support plain text of user private key.\n- [x] Support key path of user private key.\n- [x] Support Timeout for the TCP connection to establish.\n- [x] Support SSH ProxyCommand.\n\n```bash\n     +--------+       +----------+      +-----------+\n     | Laptop | <-->  | Jumphost | <--> | FooServer |\n     +--------+       +----------+      +-----------+\n\n                         OR\n\n     +--------+       +----------+      +-----------+\n     | Laptop | <-->  | Firewall | <--> | FooServer |\n     +--------+       +----------+      +-----------+\n     192.168.1.5       121.1.2.3         10.10.29.68\n```\n\n## Installation\n\n```bash\ngo get github.com/appleboy/easyssh-proxy\n```\n\n**Requirements:** Go 1.24 or higher\n\n## Usage\n\nYou can see detailed examples of the `ssh`, `scp`, `Proxy`, and `stream` commands inside the [`examples`](./_examples/) folder.\n\n### MakeConfig\n\nAll functionality provided by this package is accessed via methods of the MakeConfig struct.\n\n```go\n  ssh := &easyssh.MakeConfig{\n    User:    \"drone-scp\",\n    Server:  \"localhost\",\n    KeyPath: \"./tests/.ssh/id_rsa\",\n    Port:    \"22\",\n    Timeout: 60 * time.Second,\n  }\n\n  stdout, stderr, done, err := ssh.Run(\"ls -al\", 60*time.Second)\n  err = ssh.Scp(\"/root/source.csv\", \"/tmp/target.csv\")\n  stdoutChan, stderrChan, doneChan, errChan, err = ssh.Stream(\"for i in {1..5}; do echo ${i}; sleep 1; done; exit 2;\", 60*time.Second)\n```\n\nMakeConfig takes in the following properties:\n\n| property          | description                                                                                                                                    |\n| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |\n| user              | The SSH user to be logged in with                                                                                                              |\n| Server            | The IP or hostname pointing of the server                                                                                                      |\n| Key               | A string containing the private key to be used when making the connection                                                                      |\n| KeyPath           | The path pointing to the SSH key file to be used when making the connection                                                                    |\n| Port              | The port to use when connecting to the SSH daemon of the server                                                                                |\n| Protocol          | The tcp protocol to be used: `\"tcp\", \"tcp4\" \"tcp6\"`                                                                                            |\n| Passphrase        | The Passphrase to unlock the provided SSH key (leave blank if no Passphrase is required)                                                       |\n| Password          | The Password to use to login the specified user                                                                                                |\n| Timeout           | The length of time to wait before timing out the request                                                                                       |\n| Proxy             | An additional set of configuration params that will be used to SSH into an additional server via the server configured in this top-level block |\n| Ciphers           | An array of ciphers (e.g. aes256-ctr) to enable for the SSH connection                                                                         |\n| KeyExchanges      | An array of key exchanges (e.g. ecdh-sha2-nistp384) to enable for the SSH connection                                                           |\n| Fingerprint       | The expected fingerprint to be returned by the SSH server, results in a fingerprint error if they do not match                                 |\n| UseInsecureCipher | Enables the use of insecure ciphers and key exchanges that are insecure and can lead to compromise, [see ssh](#ssh)                            |\n\nNOTE: Please view the reference documentation for the most up to date properties of [MakeConfig](https://pkg.go.dev/github.com/appleboy/easyssh-proxy#MakeConfig) and [DefaultConfig](https://pkg.go.dev/github.com/appleboy/easyssh-proxy#DefaultConfig)\n\n### ssh\n\nSee [examples/ssh/ssh.go](./_examples/ssh/ssh.go)\n\n```go\npackage main\n\nimport (\n  \"fmt\"\n  \"time\"\n\n  \"github.com/appleboy/easyssh-proxy\"\n)\n\nfunc main() {\n  // Create MakeConfig instance with remote username, server address and path to private key.\n  ssh := &easyssh.MakeConfig{\n    User:   \"appleboy\",\n    Server: \"example.com\",\n    // Optional key or Password without either we try to contact your agent SOCKET\n    // Password: \"password\",\n    // Paste your source content of private key\n    // Key: `-----BEGIN RSA PRIVATE KEY-----\n    // MIIEpAIBAAKCAQEA4e2D/qPN08pzTac+a8ZmlP1ziJOXk45CynMPtva0rtK/RB26\n    // 7XC9wlRna4b3Ln8ew3q1ZcBjXwD4ppbTlmwAfQIaZTGJUgQbdsO9YA==\n    // -----END RSA PRIVATE KEY-----\n    // `,\n    KeyPath: \"/Users/username/.ssh/id_rsa\",\n    Port:    \"22\",\n    Timeout: 60 * time.Second,\n\n    // Parse PrivateKey With Passphrase\n    Passphrase: \"1234\",\n\n    // Optional fingerprint SHA256 verification\n    // Get Fingerprint: ssh.FingerprintSHA256(key)\n    // Fingerprint: \"SHA256:mVPwvezndPv/ARoIadVY98vAC0g+P/5633yTC4d/wXE\"\n\n    // Enable the use of insecure ciphers and key exchange methods.\n    // This enables the use of the the following insecure ciphers and key exchange methods:\n    // - aes128-cbc\n    // - aes192-cbc\n    // - aes256-cbc\n    // - 3des-cbc\n    // - diffie-hellman-group-exchange-sha256\n    // - diffie-hellman-group-exchange-sha1\n    // Those algorithms are insecure and may allow plaintext data to be recovered by an attacker.\n    // UseInsecureCipher: true,\n  }\n\n  // Call Run method with command you want to run on remote server.\n  stdout, stderr, done, err := ssh.Run(\"ls -al\", 60*time.Second)\n  // Handle errors\n  if err != nil {\n    panic(\"Can't run remote command: \" + err.Error())\n  } else {\n    fmt.Println(\"don is :\", done, \"stdout is :\", stdout, \";   stderr is :\", stderr)\n  }\n}\n```\n\n### scp\n\nSee [examples/scp/scp.go](./_examples/scp/scp.go)\n\n```go\npackage main\n\nimport (\n  \"fmt\"\n\n  \"github.com/appleboy/easyssh-proxy\"\n)\n\nfunc main() {\n  // Create MakeConfig instance with remote username, server address and path to private key.\n  ssh := &easyssh.MakeConfig{\n    User:     \"appleboy\",\n    Server:   \"example.com\",\n    Password: \"123qwe\",\n    Port:     \"22\",\n  }\n\n  // Call Scp method with file you want to upload to remote server.\n  // Please make sure the `tmp` floder exists.\n  err := ssh.Scp(\"/root/source.csv\", \"/tmp/target.csv\")\n\n  // Handle errors\n  if err != nil {\n    panic(\"Can't run remote command: \" + err.Error())\n  } else {\n    fmt.Println(\"success\")\n  }\n}\n```\n\n### SSH ProxyCommand\n\nSee [examples/proxy/proxy.go](./_examples/proxy/proxy.go)\n\n```go\n  ssh := &easyssh.MakeConfig{\n    User:    \"drone-scp\",\n    Server:  \"localhost\",\n    Port:    \"22\",\n    KeyPath: \"./tests/.ssh/id_rsa\",\n    Timeout: 60 * time.Second,\n    Proxy: easyssh.DefaultConfig{\n      User:    \"drone-scp\",\n      Server:  \"localhost\",\n      Port:    \"22\",\n      KeyPath: \"./tests/.ssh/id_rsa\",\n      Timeout: 60 * time.Second,\n    },\n  }\n```\n\nNOTE: Properties for the Proxy connection are not inherited from the Jumphost. You must explicitly specify them in the DefaultConfig struct.\n\ne.g. A custom `Timeout` length must be specified for both the Jumphost (intermediary server) and the destination server.\n\n### SSH Stream Log\n\nSee [examples/stream/stream.go](./_examples/stream/stream.go)\n\n```go\nfunc main() {\n  // Create MakeConfig instance with remote username, server address and path to private key.\n  ssh := &easyssh.MakeConfig{\n    Server:  \"localhost\",\n    User:    \"drone-scp\",\n    KeyPath: \"./tests/.ssh/id_rsa\",\n    Port:    \"22\",\n    Timeout: 60 * time.Second,\n  }\n\n  // Call Run method with command you want to run on remote server.\n  stdoutChan, stderrChan, doneChan, errChan, err := ssh.Stream(\"for i in {1..5}; do echo ${i}; sleep 1; done; exit 2;\", 60*time.Second)\n  // Handle errors\n  if err != nil {\n    panic(\"Can't run remote command: \" + err.Error())\n  } else {\n    // read from the output channel until the done signal is passed\n    isTimeout := true\n  loop:\n    for {\n      select {\n      case isTimeout = <-doneChan:\n        break loop\n      case outline := <-stdoutChan:\n        fmt.Println(\"out:\", outline)\n      case errline := <-stderrChan:\n        fmt.Println(\"err:\", errline)\n      case err = <-errChan:\n      }\n    }\n\n    // get exit code or command error.\n    if err != nil {\n      fmt.Println(\"err: \" + err.Error())\n    }\n\n    // command time out\n    if !isTimeout {\n      fmt.Println(\"Error: command timeout\")\n    }\n  }\n}\n```\n\n### WriteFile\n\nSee [examples/writeFile/writeFile.go](./_examples/writeFile/writeFile.go)\n\n```go\nfunc (ssh_conf *MakeConfig) WriteFile(reader io.Reader, size int64, etargetFile string) error\n```\n\n```go\nfunc main() {\n  // Create MakeConfig instance with remote username, server address and path to private key.\n  ssh := &easyssh.MakeConfig{\n    Server:  \"localhost\",\n    User:    \"drone-scp\",\n    KeyPath: \"./tests/.ssh/id_rsa\",\n    Port:    \"22\",\n    Timeout: 60 * time.Second,\n  }\n\n  fileContents := \"Example Text...\"\n  reader := strings.NewReader(fileContents)\n\n  // Write a file to the remote server using the writeFile command.\n  // Second argument specifies the number of bytes to write to the server from the reader.\n  if err := ssh.WriteFile(reader, int64(len(fileContents)), \"/home/user/foo.txt\"); err != nil {\n    return fmt.Errorf(\"Error: failed to write file to client. error: %w\", err)\n  }\n}\n```\n\n| property    | description                                                         |\n| ----------- | ------------------------------------------------------------------- |\n| reader      | The `io.reader` who's contents will be read and saved to the server |\n| size        | The number of bytes to be read from the `io.reader`                 |\n| etargetFile | The location on the server that the file will be written to         |\n"
  },
  {
    "path": "README.zh-tw.md",
    "content": "# easyssh-proxy\n\n[![GoDoc](https://godoc.org/github.com/appleboy/easyssh-proxy?status.svg)](https://pkg.go.dev/github.com/appleboy/easyssh-proxy)\n[![Lint and Testing](https://github.com/appleboy/easyssh-proxy/actions/workflows/testing.yml/badge.svg)](https://github.com/appleboy/easyssh-proxy/actions/workflows/testing.yml)\n[![Trivy Security Scan](https://github.com/appleboy/easyssh-proxy/actions/workflows/trivy.yml/badge.svg)](https://github.com/appleboy/easyssh-proxy/actions/workflows/trivy.yml)\n[![codecov](https://codecov.io/gh/appleboy/easyssh-proxy/branch/master/graph/badge.svg)](https://codecov.io/gh/appleboy/easyssh-proxy)\n[![Go Report Card](https://goreportcard.com/badge/github.com/appleboy/easyssh-proxy)](https://goreportcard.com/report/github.com/appleboy/easyssh-proxy)\n[![Sourcegraph](https://sourcegraph.com/github.com/appleboy/easyssh-proxy/-/badge.svg)](https://sourcegraph.com/github.com/appleboy/easyssh-proxy?badge)\n\neasyssh-proxy 提供了一個用 Go 語言實現的一些 SSH 協議功能的簡單實現。\n\n## 功能\n\n這個項目是從 [easyssh](https://github.com/hypersleep/easyssh) 分叉而來，但添加了一些如下所示的功能。\n\n- [x] 支援用戶私鑰的純文字。\n- [x] 支援用戶私鑰的路徑。\n- [x] 支援 TCP 連接建立的超時設定。\n- [x] 支援 SSH ProxyCommand。\n\n```bash\n     +--------+       +----------+      +-----------+\n     | Laptop | <-->  | Jumphost | <--> | FooServer |\n     +--------+       +----------+      +-----------+\n\n                         OR\n\n     +--------+       +----------+      +-----------+\n     | Laptop | <-->  | Firewall | <--> | FooServer |\n     +--------+       +----------+      +-----------+\n     192.168.1.5       121.1.2.3         10.10.29.68\n```\n\n## 安裝\n\n```bash\ngo get github.com/appleboy/easyssh-proxy\n```\n\n**需求：** Go 1.24 或更高版本\n\n## 使用方法\n\n你可以在 [`examples`](./_examples/) 資料夾中看到 `ssh`、`scp`、`Proxy` 和 `stream` 命令的詳細範例。\n\n### MakeConfig\n\n這個套件提供的所有功能都是通過 MakeConfig 結構體的方法來訪問的。\n\n```go\n  ssh := &easyssh.MakeConfig{\n    User:    \"drone-scp\",\n    Server:  \"localhost\",\n    KeyPath: \"./tests/.ssh/id_rsa\",\n    Port:    \"22\",\n    Timeout: 60 * time.Second,\n  }\n\n  stdout, stderr, done, err := ssh.Run(\"ls -al\", 60*time.Second)\n  err = ssh.Scp(\"/root/source.csv\", \"/tmp/target.csv\")\n  stdoutChan, stderrChan, doneChan, errChan, err = ssh.Stream(\"for i in {1..5}; do echo ${i}; sleep 1; done; exit 2;\", 60*time.Second)\n```\n\nMakeConfig 接受以下屬性：\n\n| 屬性              | 描述                                                                         |\n| ----------------- | ---------------------------------------------------------------------------- |\n| user              | 要登入的 SSH 用戶                                                            |\n| Server            | 伺服器的 IP 或主機名稱                                                       |\n| Key               | 包含用於建立連接的私鑰的字串                                                 |\n| KeyPath           | 指向用於建立連接的 SSH 密鑰文件的路徑                                        |\n| Port              | 連接到伺服器的 SSH 守護程序時使用的端口                                      |\n| Protocol          | 要使用的 TCP 協議：\"tcp\", \"tcp4\", \"tcp6\"                                     |\n| Passphrase        | 用於解鎖提供的 SSH 密鑰的密碼（如果不需要密碼，則留空）                      |\n| Password          | 用於登入指定用戶的密碼                                                       |\n| Timeout           | 請求超時前等待的時間長度                                                     |\n| Proxy             | 一組額外的配置參數，將通過此頂層塊中配置的伺服器 SSH 到另一個伺服器          |\n| Ciphers           | 用於 SSH 連接的密碼陣列（例如 aes256-ctr）                                   |\n| KeyExchanges      | 用於 SSH 連接的密鑰交換陣列（例如 ecdh-sha2-nistp384）                       |\n| Fingerprint       | SSH 伺服器返回的預期指紋，如果不匹配則會導致指紋錯誤                         |\n| UseInsecureCipher | 啟用不安全的密碼和密鑰交換，這些是不安全的，可能會導致妥協，[參見 ssh](#ssh) |\n\n注意：請查看參考文件以獲取 [MakeConfig](https://pkg.go.dev/github.com/appleboy/easyssh-proxy#MakeConfig) 和 [DefaultConfig](https://pkg.go.dev/github.com/appleboy/easyssh-proxy#DefaultConfig) 的最新屬性。\n\n### ssh\n\nSee [examples/ssh/ssh.go](./_examples/ssh/ssh.go)\n\n```go\npackage main\n\nimport (\n  \"fmt\"\n  \"time\"\n\n  \"github.com/appleboy/easyssh-proxy\"\n)\n\nfunc main() {\n  // Create MakeConfig instance with remote username, server address and path to private key.\n  ssh := &easyssh.MakeConfig{\n    User:   \"appleboy\",\n    Server: \"example.com\",\n    // Optional key or Password without either we try to contact your agent SOCKET\n    // Password: \"password\",\n    // Paste your source content of private key\n    // Key: `-----BEGIN RSA PRIVATE KEY-----\n    // MIIEpAIBAAKCAQEA4e2D/qPN08pzTac+a8ZmlP1ziJOXk45CynMPtva0rtK/RB26\n    // 7XC9wlRna4b3Ln8ew3q1ZcBjXwD4ppbTlmwAfQIaZTGJUgQbdsO9YA==\n    // -----END RSA PRIVATE KEY-----\n    // `,\n    KeyPath: \"/Users/username/.ssh/id_rsa\",\n    Port:    \"22\",\n    Timeout: 60 * time.Second,\n\n    // Parse PrivateKey With Passphrase\n    Passphrase: \"1234\",\n\n    // Optional fingerprint SHA256 verification\n    // Get Fingerprint: ssh.FingerprintSHA256(key)\n    // Fingerprint: \"SHA256:mVPwvezndPv/ARoIadVY98vAC0g+P/5633yTC4d/wXE\"\n\n    // Enable the use of insecure ciphers and key exchange methods.\n    // This enables the use of the the following insecure ciphers and key exchange methods:\n    // - aes128-cbc\n    // - aes192-cbc\n    // - aes256-cbc\n    // - 3des-cbc\n    // - diffie-hellman-group-exchange-sha256\n    // - diffie-hellman-group-exchange-sha1\n    // Those algorithms are insecure and may allow plaintext data to be recovered by an attacker.\n    // UseInsecureCipher: true,\n  }\n\n  // Call Run method with command you want to run on remote server.\n  stdout, stderr, done, err := ssh.Run(\"ls -al\", 60*time.Second)\n  // Handle errors\n  if err != nil {\n    panic(\"Can't run remote command: \" + err.Error())\n  } else {\n    fmt.Println(\"don is :\", done, \"stdout is :\", stdout, \";   stderr is :\", stderr)\n  }\n}\n```\n\n### scp\n\nSee [examples/scp/scp.go](./_examples/scp/scp.go)\n\n```go\npackage main\n\nimport (\n  \"fmt\"\n\n  \"github.com/appleboy/easyssh-proxy\"\n)\n\nfunc main() {\n  // Create MakeConfig instance with remote username, server address and path to private key.\n  ssh := &easyssh.MakeConfig{\n    User:     \"appleboy\",\n    Server:   \"example.com\",\n    Password: \"123qwe\",\n    Port:     \"22\",\n  }\n\n  // Call Scp method with file you want to upload to remote server.\n  // Please make sure the `tmp` folder exists.\n  err := ssh.Scp(\"/root/source.csv\", \"/tmp/target.csv\")\n\n  // Handle errors\n  if err != nil {\n    panic(\"Can't run remote command: \" + err.Error())\n  } else {\n    fmt.Println(\"success\")\n  }\n}\n```\n\n### SSH ProxyCommand\n\nSee [examples/proxy/proxy.go](./_examples/proxy/proxy.go)\n\n```go\n  ssh := &easyssh.MakeConfig{\n    User:    \"drone-scp\",\n    Server:  \"localhost\",\n    Port:    \"22\",\n    KeyPath: \"./tests/.ssh/id_rsa\",\n    Timeout: 60 * time.Second,\n    Proxy: easyssh.DefaultConfig{\n      User:    \"drone-scp\",\n      Server:  \"localhost\",\n      Port:    \"22\",\n      KeyPath: \"./tests/.ssh/id_rsa\",\n      Timeout: 60 * time.Second,\n    },\n  }\n```\n\n注意：代理連接的屬性不會從跳板機繼承。您必須在 DefaultConfig 結構體中明確指定它們。\n\n例如，必須為跳板機（中介伺服器）和目標伺服器分別指定自定義的 `Timeout` 長度。\n\n### SSH Stream Log\n\nSee [examples/stream/stream.go](./_examples/stream/stream.go)\n\n```go\nfunc main() {\n  // Create MakeConfig instance with remote username, server address and path to private key.\n  ssh := &easyssh.MakeConfig{\n    Server:  \"localhost\",\n    User:    \"drone-scp\",\n    KeyPath: \"./tests/.ssh/id_rsa\",\n    Port:    \"22\",\n    Timeout: 60 * time.Second,\n  }\n\n  // Call Run method with command you want to run on remote server.\n  stdoutChan, stderrChan, doneChan, errChan, err := ssh.Stream(\"for i in {1..5}; do echo ${i}; sleep 1; done; exit 2;\", 60*time.Second)\n  // Handle errors\n  if err != nil {\n    panic(\"Can't run remote command: \" + err.Error())\n  } else {\n    // read from the output channel until the done signal is passed\n    isTimeout := true\n  loop:\n    for {\n      select {\n      case isTimeout = <-doneChan:\n        break loop\n      case outline := <-stdoutChan:\n        fmt.Println(\"out:\", outline)\n      case errline := <-stderrChan:\n        fmt.Println(\"err:\", errline)\n      case err = <-errChan:\n      }\n    }\n\n    // get exit code or command error.\n    if err != nil {\n      fmt.Println(\"err: \" + err.Error())\n    }\n\n    // command time out\n    if !isTimeout {\n      fmt.Println(\"Error: command timeout\")\n    }\n  }\n}\n```\n\n### WriteFile\n\n參見 [examples/writeFile/writeFile.go](./_examples/writeFile/writeFile.go)\n\n```go\nfunc (ssh_conf *MakeConfig) WriteFile(reader io.Reader, size int64, etargetFile string) error\n```\n\n```go\nfunc main() {\n  // 使用遠程用戶名、伺服器地址和私鑰路徑創建 MakeConfig 實例。\n  ssh := &easyssh.MakeConfig{\n    Server:  \"localhost\",\n    User:    \"drone-scp\",\n    KeyPath: \"./tests/.ssh/id_rsa\",\n    Port:    \"22\",\n    Timeout: 60 * time.Second,\n  }\n\n  fileContents := \"Example Text...\"\n  reader := strings.NewReader(fileContents)\n\n  // 使用 writeFile 命令將文件寫入到遠程伺服器。\n  // 第二個參數指定從 reader 中寫入到伺服器的字節數。\n  if err := ssh.WriteFile(reader, int64(len(fileContents)), \"/home/user/foo.txt\"); err != nil {\n    return fmt.Errorf(\"錯誤：無法將文件寫入到客戶端。錯誤：%w\", err)\n  }\n}\n```\n\n| 屬性        | 描述                                          |\n| ----------- | --------------------------------------------- |\n| reader      | 將讀取其內容並保存到伺服器的 `io.reader`      |\n| size        | 要從 `io.reader` 中讀取的字節數               |\n| etargetFile | 文件將被寫入到伺服器上的位置                   |\n"
  },
  {
    "path": "_examples/proxy/go.mod",
    "content": "module example\n\ngo 1.24.0\n\nrequire github.com/appleboy/easyssh-proxy v1.5.0\n\nrequire (\n\tgithub.com/ScaleFT/sshkeys v1.4.0 // indirect\n\tgithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect\n\tgolang.org/x/crypto v0.43.0 // indirect\n\tgolang.org/x/sys v0.37.0 // indirect\n)\n\nreplace github.com/appleboy/easyssh-proxy v1.5.0 => ../../\n"
  },
  {
    "path": "_examples/proxy/go.sum",
    "content": "github.com/ScaleFT/sshkeys v1.4.0 h1:Yqd0cKA5PUvwV0dgRI67BDHGTsMHtGQBZbLXh1dthmE=\ngithub.com/ScaleFT/sshkeys v1.4.0/go.mod h1:GineMkS8SEiELq8q5DzA2Wnrw65SqdD9a+hm8JOU1I4=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU=\ngithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngolang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=\ngolang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=\ngolang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=\ngolang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=\ngolang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/proxy/proxy.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/appleboy/easyssh-proxy\"\n)\n\nfunc main() {\n\t// Create MakeConfig instance with remote username, server address and path to private key.\n\tssh := &easyssh.MakeConfig{\n\t\tUser:    \"drone-scp\",\n\t\tServer:  \"localhost\",\n\t\tPort:    \"22\",\n\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\tProxy: easyssh.DefaultConfig{\n\t\t\tUser:    \"drone-scp\",\n\t\t\tServer:  \"localhost\",\n\t\t\tPort:    \"22\",\n\t\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\t},\n\t}\n\n\t// Call Scp method with file you want to upload to remote server.\n\t// Please make sure the `tmp` floder exists.\n\terr := ssh.Scp(\"/root/source.csv\", \"/tmp/target.csv\")\n\tif err != nil {\n\t\tpanic(\"Can't run remote command: \" + err.Error())\n\t}\n\tfmt.Println(\"success\")\n}\n"
  },
  {
    "path": "_examples/scp/go.mod",
    "content": "module example\n\ngo 1.24.0\n\nrequire github.com/appleboy/easyssh-proxy v1.5.0\n\nrequire (\n\tgithub.com/ScaleFT/sshkeys v1.4.0 // indirect\n\tgithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect\n\tgolang.org/x/crypto v0.43.0 // indirect\n\tgolang.org/x/sys v0.37.0 // indirect\n)\n\nreplace github.com/appleboy/easyssh-proxy v1.5.0 => ../../\n"
  },
  {
    "path": "_examples/scp/go.sum",
    "content": "github.com/ScaleFT/sshkeys v1.4.0 h1:Yqd0cKA5PUvwV0dgRI67BDHGTsMHtGQBZbLXh1dthmE=\ngithub.com/ScaleFT/sshkeys v1.4.0/go.mod h1:GineMkS8SEiELq8q5DzA2Wnrw65SqdD9a+hm8JOU1I4=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU=\ngithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngolang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=\ngolang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=\ngolang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=\ngolang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=\ngolang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/scp/scp.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/appleboy/easyssh-proxy\"\n)\n\nfunc main() {\n\t// Create MakeConfig instance with remote username, server address and path to private key.\n\tssh := &easyssh.MakeConfig{\n\t\tUser:     \"appleboy\",\n\t\tServer:   \"example.com\",\n\t\tPassword: \"123qwe\",\n\t\tPort:     \"22\",\n\t}\n\n\t// Call Scp method with file you want to upload to remote server.\n\t// Please make sure the `tmp` floder exists.\n\terr := ssh.Scp(\"/root/source.csv\", \"/tmp/target.csv\")\n\t// Handle errors\n\tif err != nil {\n\t\tpanic(\"Can't run remote command: \" + err.Error())\n\t}\n\tfmt.Println(\"success\")\n}\n"
  },
  {
    "path": "_examples/ssh/go.mod",
    "content": "module example\n\ngo 1.24.0\n\nrequire github.com/appleboy/easyssh-proxy v1.5.0\n\nrequire (\n\tgithub.com/ScaleFT/sshkeys v1.4.0 // indirect\n\tgithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect\n\tgolang.org/x/crypto v0.43.0 // indirect\n\tgolang.org/x/sys v0.37.0 // indirect\n)\n\nreplace github.com/appleboy/easyssh-proxy v1.5.0 => ../../\n"
  },
  {
    "path": "_examples/ssh/go.sum",
    "content": "github.com/ScaleFT/sshkeys v1.4.0 h1:Yqd0cKA5PUvwV0dgRI67BDHGTsMHtGQBZbLXh1dthmE=\ngithub.com/ScaleFT/sshkeys v1.4.0/go.mod h1:GineMkS8SEiELq8q5DzA2Wnrw65SqdD9a+hm8JOU1I4=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU=\ngithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngolang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=\ngolang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=\ngolang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=\ngolang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=\ngolang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/ssh/ssh.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/appleboy/easyssh-proxy\"\n)\n\nfunc main() {\n\t// Create MakeConfig instance with remote username, server address and path to private key.\n\tssh := &easyssh.MakeConfig{\n\t\tUser:   \"appleboy\",\n\t\tServer: \"example.com\",\n\t\t// Optional key or Password without either we try to contact your agent SOCKET\n\t\t// Password: \"password\",\n\t\t// Paste your source content of private key\n\t\t// Key: `-----BEGIN RSA PRIVATE KEY-----\n\t\t// MIIEpAIBAAKCAQEA4e2D/qPN08pzTac+a8ZmlP1ziJOXk45CynMPtva0rtK/RB26\n\t\t// 7XC9wlRna4b3Ln8ew3q1ZcBjXwD4ppbTlmwAfQIaZTGJUgQbdsO9YA==\n\t\t// -----END RSA PRIVATE KEY-----\n\t\t// `,\n\t\tKeyPath: \"/Users/username/.ssh/id_rsa\",\n\t\tPort:    \"22\",\n\t\tTimeout: 60 * time.Second,\n\n\t\t// Parse PrivateKey With Passphrase\n\t\tPassphrase: \"1234\",\n\n\t\t// Optional fingerprint SHA256 verification\n\t\t// Get Fingerprint: ssh.FingerprintSHA256(key)\n\t\t// Fingerprint: \"SHA256:mVPwvezndPv/ARoIadVY98vAC0g+P/5633yTC4d/wXE\"\n\n\t\t// Enable the use of insecure ciphers and key exchange methods.\n\t\t// This enables the use of the the following insecure ciphers and key exchange methods:\n\t\t// - aes128-cbc\n\t\t// - aes192-cbc\n\t\t// - aes256-cbc\n\t\t// - 3des-cbc\n\t\t// - diffie-hellman-group-exchange-sha256\n\t\t// - diffie-hellman-group-exchange-sha1\n\t\t// Those algorithms are insecure and may allow plaintext data to be recovered by an attacker.\n\t\t// UseInsecureCipher: true,\n\t}\n\n\t// Call Run method with command you want to run on remote server.\n\tstdout, stderr, done, err := ssh.Run(\"ls -al\", 60*time.Second)\n\t// Handle errors\n\tif err != nil {\n\t\tpanic(\"Can't run remote command: \" + err.Error())\n\t}\n\tfmt.Println(\"don is :\", done, \"stdout is :\", stdout, \";   stderr is :\", stderr)\n}\n"
  },
  {
    "path": "_examples/stream/go.mod",
    "content": "module example\n\ngo 1.24.0\n\nrequire github.com/appleboy/easyssh-proxy v1.5.0\n\nrequire (\n\tgithub.com/ScaleFT/sshkeys v1.4.0 // indirect\n\tgithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect\n\tgolang.org/x/crypto v0.43.0 // indirect\n\tgolang.org/x/sys v0.37.0 // indirect\n)\n\nreplace github.com/appleboy/easyssh-proxy v1.5.0 => ../../\n"
  },
  {
    "path": "_examples/stream/go.sum",
    "content": "github.com/ScaleFT/sshkeys v1.4.0 h1:Yqd0cKA5PUvwV0dgRI67BDHGTsMHtGQBZbLXh1dthmE=\ngithub.com/ScaleFT/sshkeys v1.4.0/go.mod h1:GineMkS8SEiELq8q5DzA2Wnrw65SqdD9a+hm8JOU1I4=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU=\ngithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngolang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=\ngolang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=\ngolang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=\ngolang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=\ngolang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/stream/stream.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/appleboy/easyssh-proxy\"\n)\n\nfunc main() {\n\t// Create MakeConfig instance with remote username, server address and path to private key.\n\tssh := &easyssh.MakeConfig{\n\t\tServer:  \"localhost\",\n\t\tUser:    \"drone-scp\",\n\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\tPort:    \"22\",\n\t\tTimeout: 60 * time.Second,\n\t}\n\n\t// Call Run method with command you want to run on remote server.\n\tstdoutChan, stderrChan, doneChan, errChan, err := ssh.Stream(\"for i in {1..5}; do echo ${i}; sleep 1; done; exit 2;\", 60*time.Second)\n\t// Handle errors\n\tif err != nil {\n\t\tpanic(\"Can't run remote command: \" + err.Error())\n\t}\n\t// read from the output channel until the done signal is passed\n\tisTimeout := true\nloop:\n\tfor {\n\t\tselect {\n\t\tcase isTimeout = <-doneChan:\n\t\t\tbreak loop\n\t\tcase outline := <-stdoutChan:\n\t\t\tfmt.Println(\"out:\", outline)\n\t\tcase errline := <-stderrChan:\n\t\t\tfmt.Println(\"err:\", errline)\n\t\tcase err = <-errChan:\n\t\t}\n\t}\n\n\t// get exit code or command error.\n\tif err != nil {\n\t\tpanic(\"err: \" + err.Error())\n\t}\n\n\t// command time out\n\tif !isTimeout {\n\t\tfmt.Println(\"Error: command timeout\")\n\t}\n}\n"
  },
  {
    "path": "_examples/writeFile/go.mod",
    "content": "module example\n\ngo 1.24.0\n\nrequire github.com/appleboy/easyssh-proxy v1.5.0\n\nrequire (\n\tgithub.com/ScaleFT/sshkeys v1.4.0 // indirect\n\tgithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect\n\tgolang.org/x/crypto v0.43.0 // indirect\n\tgolang.org/x/sys v0.37.0 // indirect\n)\n\nreplace github.com/appleboy/easyssh-proxy v1.5.0 => ../../\n"
  },
  {
    "path": "_examples/writeFile/go.sum",
    "content": "github.com/ScaleFT/sshkeys v1.4.0 h1:Yqd0cKA5PUvwV0dgRI67BDHGTsMHtGQBZbLXh1dthmE=\ngithub.com/ScaleFT/sshkeys v1.4.0/go.mod h1:GineMkS8SEiELq8q5DzA2Wnrw65SqdD9a+hm8JOU1I4=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU=\ngithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngolang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=\ngolang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=\ngolang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=\ngolang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=\ngolang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "_examples/writeFile/writeFile.go",
    "content": "package main\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/appleboy/easyssh-proxy\"\n)\n\nfunc main() {\n\t// Create MakeConfig instance with remote username, server address and path to private key.\n\tssh := &easyssh.MakeConfig{\n\t\tUser:    \"appleboy\",\n\t\tServer:  \"example.com\",\n\t\tKeyPath: \"/Users/username/.ssh/id_rsa\",\n\t\tPort:    \"22\",\n\t\tTimeout: 60 * time.Second,\n\t}\n\n\tfileContents := \"Example Text...\"\n\treader := strings.NewReader(fileContents)\n\n\t// Write a file to the remote server using the writeFile command.\n\t// Second arguement specifies the number of bytes to write to the server from the reader.\n\tif err := ssh.WriteFile(reader, int64(len(fileContents)), \"/home/user/foo.txt\"); err != nil {\n\t\tpanic(\"Error: failed to write file to client\")\n\t}\n}\n"
  },
  {
    "path": "easyssh.go",
    "content": "// Package easyssh provides a simple implementation of some SSH protocol\n// features in Go. You can simply run a command on a remote server or get a file\n// even simpler than native console SSH client. You don't need to think about\n// Dials, sessions, defers, or public keys... Let easyssh think about it!\npackage easyssh\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/ScaleFT/sshkeys\"\n\t\"golang.org/x/crypto/ssh\"\n\t\"golang.org/x/crypto/ssh/agent\"\n)\n\nvar (\n\tdefaultTimeout    = 60 * time.Second\n\tdefaultBufferSize = 4096\n)\n\nvar (\n\t// ErrProxyDialTimeout is returned when proxy dial connection times out\n\tErrProxyDialTimeout = errors.New(\"proxy dial timeout\")\n)\n\ntype Protocol string\n\nconst (\n\tPROTOCOL_TCP  Protocol = \"tcp\"\n\tPROTOCOL_TCP4 Protocol = \"tcp4\"\n\tPROTOCOL_TCP6 Protocol = \"tcp6\"\n)\n\ntype (\n\t// MakeConfig Contains main authority information.\n\t// User field should be a name of user on remote server (ex. john in ssh john@example.com).\n\t// Server field should be a remote machine address (ex. example.com in ssh john@example.com)\n\t// Key is a path to private key on your local machine.\n\t// Port is SSH server port on remote machine.\n\t// Note: easyssh looking for private key in user's home directory (ex. /home/john + Key).\n\t// Then ensure your Key begins from '/' (ex. /.ssh/id_rsa)\n\tMakeConfig struct {\n\t\tUser         string\n\t\tServer       string\n\t\tKey          string\n\t\tKeyPath      string\n\t\tPort         string\n\t\tProtocol     Protocol\n\t\tPassphrase   string\n\t\tPassword     string\n\t\tTimeout      time.Duration\n\t\tProxy        DefaultConfig\n\t\tReadBuffSize int\n\t\tCiphers      []string\n\t\tKeyExchanges []string\n\t\tFingerprint  string\n\n\t\t// Enable the use of insecure ciphers and key exchange methods.\n\t\t// This enables the use of the the following insecure ciphers and key exchange methods:\n\t\t// - aes128-cbc\n\t\t// - aes192-cbc\n\t\t// - aes256-cbc\n\t\t// - 3des-cbc\n\t\t// - diffie-hellman-group-exchange-sha256\n\t\t// - diffie-hellman-group-exchange-sha1\n\t\t// Those algorithms are insecure and may allow plaintext data to be recovered by an attacker.\n\t\tUseInsecureCipher bool\n\n\t\t// RequestPty requests a pseudo-terminal from the server.\n\t\tRequestPty bool\n\t}\n\n\t// DefaultConfig for ssh proxy config\n\tDefaultConfig struct {\n\t\tUser         string\n\t\tServer       string\n\t\tKey          string\n\t\tKeyPath      string\n\t\tPort         string\n\t\tProtocol     Protocol\n\t\tPassphrase   string\n\t\tPassword     string\n\t\tTimeout      time.Duration\n\t\tCiphers      []string\n\t\tKeyExchanges []string\n\t\tFingerprint  string\n\n\t\t// Enable the use of insecure ciphers and key exchange methods.\n\t\t// This enables the use of the the following insecure ciphers and key exchange methods:\n\t\t// - aes128-cbc\n\t\t// - aes192-cbc\n\t\t// - aes256-cbc\n\t\t// - 3des-cbc\n\t\t// - diffie-hellman-group-exchange-sha256\n\t\t// - diffie-hellman-group-exchange-sha1\n\t\t// Those algorithms are insecure and may allow plaintext data to be recovered by an attacker.\n\t\tUseInsecureCipher bool\n\t}\n)\n\n// returns ssh.Signer from user you running app home path + cutted key path.\n// (ex. pubkey,err := getKeyFile(\"/.ssh/id_rsa\") )\nfunc getKeyFile(keypath, passphrase string) (ssh.Signer, error) {\n\tvar pubkey ssh.Signer\n\tvar err error\n\tbuf, err := os.ReadFile(keypath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif passphrase != \"\" {\n\t\tpubkey, err = sshkeys.ParseEncryptedPrivateKey(buf, []byte(passphrase))\n\t} else {\n\t\tpubkey, err = ssh.ParsePrivateKey(buf)\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn pubkey, nil\n}\n\n// returns *ssh.ClientConfig and io.Closer.\n// if io.Closer is not nil, io.Closer.Close() should be called when\n// *ssh.ClientConfig is no longer used.\nfunc getSSHConfig(config DefaultConfig) (*ssh.ClientConfig, io.Closer) {\n\tvar sshAgent io.Closer\n\n\t// auths holds the detected ssh auth methods\n\tauths := []ssh.AuthMethod{}\n\n\t// figure out what auths are requested, what is supported\n\tif config.Password != \"\" {\n\t\tauths = append(auths, ssh.Password(config.Password))\n\t}\n\tif config.KeyPath != \"\" {\n\t\tif pubkey, err := getKeyFile(config.KeyPath, config.Passphrase); err != nil {\n\t\t\tlog.Printf(\"getKeyFile error: %v\\n\", err)\n\t\t} else {\n\t\t\tauths = append(auths, ssh.PublicKeys(pubkey))\n\t\t}\n\t}\n\n\tif config.Key != \"\" {\n\t\tvar signer ssh.Signer\n\t\tvar err error\n\t\tif config.Passphrase != \"\" {\n\t\t\tsigner, err = sshkeys.ParseEncryptedPrivateKey([]byte(config.Key), []byte(config.Passphrase))\n\t\t} else {\n\t\t\tsigner, err = ssh.ParsePrivateKey([]byte(config.Key))\n\t\t}\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"ssh.ParsePrivateKey: %v\\n\", err)\n\t\t} else {\n\t\t\tauths = append(auths, ssh.PublicKeys(signer))\n\t\t}\n\t}\n\n\tif sshAgent, err := net.Dial(\"unix\", os.Getenv(\"SSH_AUTH_SOCK\")); err == nil {\n\t\tauths = append(auths, ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers))\n\t}\n\n\tc := ssh.Config{}\n\tif config.UseInsecureCipher {\n\t\tc.SetDefaults()\n\t\tc.Ciphers = append(c.Ciphers, \"aes128-cbc\", \"aes192-cbc\", \"aes256-cbc\", \"3des-cbc\")\n\t\tc.KeyExchanges = append(c.KeyExchanges, \"diffie-hellman-group-exchange-sha1\", \"diffie-hellman-group-exchange-sha256\")\n\t}\n\n\tif len(config.Ciphers) > 0 {\n\t\tc.Ciphers = append(c.Ciphers, config.Ciphers...)\n\t}\n\n\tif len(config.KeyExchanges) > 0 {\n\t\tc.KeyExchanges = append(c.KeyExchanges, config.KeyExchanges...)\n\t}\n\n\thostKeyCallback := ssh.InsecureIgnoreHostKey()\n\tif config.Fingerprint != \"\" {\n\t\thostKeyCallback = func(hostname string, remote net.Addr, publicKey ssh.PublicKey) error {\n\t\t\tif ssh.FingerprintSHA256(publicKey) != config.Fingerprint {\n\t\t\t\treturn fmt.Errorf(\"ssh: host key fingerprint mismatch\")\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn &ssh.ClientConfig{\n\t\tConfig:          c,\n\t\tTimeout:         config.Timeout,\n\t\tUser:            config.User,\n\t\tAuth:            auths,\n\t\tHostKeyCallback: hostKeyCallback,\n\t}, sshAgent\n}\n\n// Connect to remote server using MakeConfig struct and returns *ssh.Session\nfunc (ssh_conf *MakeConfig) Connect() (*ssh.Session, *ssh.Client, error) {\n\tvar client *ssh.Client\n\tvar err error\n\n\t// Default protocol is: tcp.\n\tif ssh_conf.Protocol == \"\" {\n\t\tssh_conf.Protocol = PROTOCOL_TCP\n\t}\n\tif ssh_conf.Proxy.Protocol == \"\" {\n\t\tssh_conf.Proxy.Protocol = PROTOCOL_TCP\n\t}\n\n\ttargetConfig, closer := getSSHConfig(DefaultConfig{\n\t\tUser:              ssh_conf.User,\n\t\tKey:               ssh_conf.Key,\n\t\tKeyPath:           ssh_conf.KeyPath,\n\t\tPassphrase:        ssh_conf.Passphrase,\n\t\tPassword:          ssh_conf.Password,\n\t\tTimeout:           ssh_conf.Timeout,\n\t\tCiphers:           ssh_conf.Ciphers,\n\t\tKeyExchanges:      ssh_conf.KeyExchanges,\n\t\tFingerprint:       ssh_conf.Fingerprint,\n\t\tUseInsecureCipher: ssh_conf.UseInsecureCipher,\n\t})\n\tif closer != nil {\n\t\tdefer closer.Close()\n\t}\n\n\t// Enable proxy command\n\tif ssh_conf.Proxy.Server != \"\" {\n\t\tproxyConfig, closer := getSSHConfig(DefaultConfig{\n\t\t\tUser:              ssh_conf.Proxy.User,\n\t\t\tKey:               ssh_conf.Proxy.Key,\n\t\t\tKeyPath:           ssh_conf.Proxy.KeyPath,\n\t\t\tPassphrase:        ssh_conf.Proxy.Passphrase,\n\t\t\tPassword:          ssh_conf.Proxy.Password,\n\t\t\tTimeout:           ssh_conf.Proxy.Timeout,\n\t\t\tCiphers:           ssh_conf.Proxy.Ciphers,\n\t\t\tKeyExchanges:      ssh_conf.Proxy.KeyExchanges,\n\t\t\tFingerprint:       ssh_conf.Proxy.Fingerprint,\n\t\t\tUseInsecureCipher: ssh_conf.Proxy.UseInsecureCipher,\n\t\t})\n\t\tif closer != nil {\n\t\t\tdefer closer.Close()\n\t\t}\n\n\t\tproxyClient, err := ssh.Dial(string(ssh_conf.Proxy.Protocol), net.JoinHostPort(ssh_conf.Proxy.Server, ssh_conf.Proxy.Port), proxyConfig)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\n\t\t// Apply timeout to the connection from proxy to target server\n\t\ttimeout := ssh_conf.Timeout\n\t\tif timeout == 0 {\n\t\t\ttimeout = defaultTimeout\n\t\t}\n\n\t\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\t\tdefer cancel()\n\n\t\ttype connResult struct {\n\t\t\tconn net.Conn\n\t\t\terr  error\n\t\t}\n\n\t\tconnCh := make(chan connResult, 1)\n\t\tgo func() {\n\t\t\tconn, err := proxyClient.Dial(string(ssh_conf.Protocol), net.JoinHostPort(ssh_conf.Server, ssh_conf.Port))\n\t\t\tselect {\n\t\t\tcase connCh <- connResult{conn: conn, err: err}:\n\t\t\t\t// Successfully sent result\n\t\t\tcase <-ctx.Done():\n\t\t\t\t// Context was cancelled, clean up the connection if it was established\n\t\t\t\tif conn != nil {\n\t\t\t\t\tconn.Close()\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\n\t\tvar conn net.Conn\n\t\tselect {\n\t\tcase result := <-connCh:\n\t\t\tconn = result.conn\n\t\t\terr = result.err\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, nil, fmt.Errorf(\"%w: %v\", ErrProxyDialTimeout, ctx.Err())\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\n\t\tncc, chans, reqs, err := ssh.NewClientConn(conn, net.JoinHostPort(ssh_conf.Server, ssh_conf.Port), targetConfig)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\n\t\tclient = ssh.NewClient(ncc, chans, reqs)\n\t} else {\n\t\tclient, err = ssh.Dial(string(ssh_conf.Protocol), net.JoinHostPort(ssh_conf.Server, ssh_conf.Port), targetConfig)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t}\n\n\tsession, err := client.NewSession()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// Request a pseudo-terminal if this option is set\n\tif ssh_conf.RequestPty {\n\t\tmodes := ssh.TerminalModes{\n\t\t\tssh.ECHO:          0,     // disable echoing\n\t\t\tssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud\n\t\t\tssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud\n\t\t}\n\t\tif err := session.RequestPty(\"xterm\", 80, 40, modes); err != nil {\n\t\t\tsession.Close()\n\t\t\treturn nil, nil, err\n\t\t}\n\t}\n\n\treturn session, client, nil\n}\n\n// Stream returns one channel that combines the stdout and stderr of the command\n// as it is run on the remote machine, and another that sends true when the\n// command is done. The sessions and channels will then be closed.\nfunc (ssh_conf *MakeConfig) Stream(command string, timeout ...time.Duration) (<-chan string, <-chan string, <-chan bool, <-chan error, error) {\n\t// continuously send the command's output over the channel\n\tstdoutChan := make(chan string)\n\tstderrChan := make(chan string)\n\tdoneChan := make(chan bool)\n\terrChan := make(chan error)\n\n\t// connect to remote host\n\tsession, client, err := ssh_conf.Connect()\n\tif err != nil {\n\t\treturn stdoutChan, stderrChan, doneChan, errChan, err\n\t}\n\t// defer session.Close()\n\t// connect to both outputs (they are of type io.Reader)\n\toutReader, err := session.StdoutPipe()\n\tif err != nil {\n\t\tclient.Close()\n\t\tsession.Close()\n\t\treturn stdoutChan, stderrChan, doneChan, errChan, err\n\t}\n\terrReader, err := session.StderrPipe()\n\tif err != nil {\n\t\tclient.Close()\n\t\tsession.Close()\n\t\treturn stdoutChan, stderrChan, doneChan, errChan, err\n\t}\n\terr = session.Start(command)\n\tif err != nil {\n\t\tclient.Close()\n\t\tsession.Close()\n\t\treturn stdoutChan, stderrChan, doneChan, errChan, err\n\t}\n\n\t// combine outputs, create a line-by-line scanner\n\tstdoutReader := io.MultiReader(outReader)\n\tstderrReader := io.MultiReader(errReader)\n\n\tvar stdoutScanner *bufio.Reader\n\tvar stderrScanner *bufio.Reader\n\n\tif ssh_conf.ReadBuffSize > 0 {\n\t\tstdoutScanner = bufio.NewReaderSize(stdoutReader, ssh_conf.ReadBuffSize)\n\t} else {\n\t\tstdoutScanner = bufio.NewReaderSize(stdoutReader, defaultBufferSize)\n\t}\n\n\tif ssh_conf.ReadBuffSize > 0 {\n\t\tstderrScanner = bufio.NewReaderSize(stderrReader, ssh_conf.ReadBuffSize)\n\t} else {\n\t\tstderrScanner = bufio.NewReaderSize(stderrReader, defaultBufferSize)\n\t}\n\n\tgo func(stdoutScanner, stderrScanner *bufio.Reader, stdoutChan, stderrChan chan string, doneChan chan bool, errChan chan error) {\n\t\tdefer close(doneChan)\n\t\tdefer close(errChan)\n\t\tdefer client.Close()\n\t\tdefer session.Close()\n\n\t\t// default timeout value\n\t\texecuteTimeout := defaultTimeout\n\t\tif len(timeout) > 0 {\n\t\t\texecuteTimeout = timeout[0]\n\t\t}\n\t\tctxTimeout, cancel := context.WithTimeout(context.Background(), executeTimeout)\n\t\tdefer cancel()\n\t\tres := make(chan struct{}, 1)\n\t\tvar resWg sync.WaitGroup\n\t\tresWg.Add(2)\n\n\t\tgo func() {\n\t\t\tdefer close(stdoutChan)\n\t\t\tfor {\n\t\t\t\tvar text string\n\t\t\t\ttext, err = stdoutScanner.ReadString('\\n')\n\t\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tstdoutChan <- strings.TrimRight(text, \"\\n\")\n\t\t\t}\n\t\t\tresWg.Done()\n\t\t}()\n\n\t\tgo func() {\n\t\t\tdefer close(stderrChan)\n\t\t\tfor {\n\t\t\t\tvar text string\n\t\t\t\ttext, err = stderrScanner.ReadString('\\n')\n\t\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tstderrChan <- strings.TrimRight(text, \"\\n\")\n\t\t\t}\n\t\t\tresWg.Done()\n\t\t}()\n\n\t\tgo func() {\n\t\t\tresWg.Wait()\n\t\t\t// close all of our open resources\n\t\t\tres <- struct{}{}\n\t\t}()\n\n\t\tselect {\n\t\tcase <-res:\n\t\t\terrChan <- session.Wait()\n\t\t\tdoneChan <- true\n\t\tcase <-ctxTimeout.Done():\n\t\t\terrChan <- fmt.Errorf(\"Run Command Timeout: %v\", ctxTimeout.Err())\n\t\t\tdoneChan <- false\n\t\t}\n\t}(stdoutScanner, stderrScanner, stdoutChan, stderrChan, doneChan, errChan)\n\n\treturn stdoutChan, stderrChan, doneChan, errChan, err\n}\n\n// Run command on remote machine and returns its stdout as a string\nfunc (ssh_conf *MakeConfig) Run(command string, timeout ...time.Duration) (outStr string, errStr string, isTimeout bool, err error) {\n\tstdoutChan, stderrChan, doneChan, errChan, err := ssh_conf.Stream(command, timeout...)\n\tif err != nil {\n\t\t// Check if the error is from a proxy dial timeout\n\t\tif errors.Is(err, ErrProxyDialTimeout) {\n\t\t\tisTimeout = true\n\t\t}\n\t\treturn outStr, errStr, isTimeout, err\n\t}\n\t// read from the output channel until the done signal is passed\nloop:\n\tfor {\n\t\tselect {\n\t\tcase isTimeout = <-doneChan:\n\t\t\tbreak loop\n\t\tcase outline, ok := <-stdoutChan:\n\t\t\tif !ok {\n\t\t\t\tstdoutChan = nil\n\t\t\t}\n\t\t\tif outline != \"\" {\n\t\t\t\toutStr += outline + \"\\n\"\n\t\t\t}\n\t\tcase errline, ok := <-stderrChan:\n\t\t\tif !ok {\n\t\t\t\tstderrChan = nil\n\t\t\t}\n\t\t\tif errline != \"\" {\n\t\t\t\terrStr += errline + \"\\n\"\n\t\t\t}\n\t\tcase err = <-errChan:\n\t\t}\n\t}\n\t// return the concatenation of all signals from the output channel\n\treturn outStr, errStr, isTimeout, err\n}\n\n// WriteFile reads size bytes from the reader and writes them to a file on the remote machine\nfunc (ssh_conf *MakeConfig) WriteFile(reader io.Reader, size int64, etargetFile string) error {\n\tsession, client, err := ssh_conf.Connect()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer client.Close()\n\tdefer session.Close()\n\n\ttargetFile := filepath.Base(etargetFile)\n\n\tw, err := session.StdinPipe()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcopyF := func() error {\n\t\t_, err := fmt.Fprintln(w, \"C0644\", size, targetFile)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif size > 0 {\n\t\t\t_, err = io.Copy(w, reader)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t_, err = fmt.Fprint(w, \"\\x00\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tcopyErrC := make(chan error, 1)\n\tgo func() {\n\t\tdefer w.Close()\n\t\tcopyErrC <- copyF()\n\t}()\n\n\terr = session.Run(fmt.Sprintf(\"scp -tr %s\", etargetFile))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = <-copyErrC\n\treturn err\n}\n\n// Scp uploads sourceFile to remote machine like native scp console app.\nfunc (ssh_conf *MakeConfig) Scp(sourceFile string, etargetFile string) error {\n\tsession, client, err := ssh_conf.Connect()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer client.Close()\n\tdefer session.Close()\n\n\tsrc, srcErr := os.Open(sourceFile)\n\n\tif srcErr != nil {\n\t\treturn srcErr\n\t}\n\tdefer src.Close()\n\n\tsrcStat, statErr := src.Stat()\n\n\tif statErr != nil {\n\t\treturn statErr\n\t}\n\treturn ssh_conf.WriteFile(src, srcStat.Size(), etargetFile)\n}\n"
  },
  {
    "path": "easyssh_test.go",
    "content": "package easyssh\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os\"\n\t\"os/user\"\n\t\"path\"\n\t\"runtime\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc getHostPublicKeyFile(keypath string) (ssh.PublicKey, error) {\n\tvar pubkey ssh.PublicKey\n\tvar err error\n\tbuf, err := os.ReadFile(keypath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpubkey, _, _, _, err = ssh.ParseAuthorizedKey(buf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn pubkey, nil\n}\n\nfunc TestGetKeyFile(t *testing.T) {\n\t// missing file\n\t_, err := getKeyFile(\"abc\", \"\")\n\tassert.Error(t, err)\n\tassert.Equal(t, \"open abc: no such file or directory\", err.Error())\n\n\t// wrong format\n\t_, err = getKeyFile(\"./tests/.ssh/id_rsa.pub\", \"\")\n\tassert.Error(t, err)\n\tassert.Equal(t, \"ssh: no key found\", err.Error())\n\n\t_, err = getKeyFile(\"./tests/.ssh/id_rsa\", \"\")\n\tassert.NoError(t, err)\n\n\t_, err = getKeyFile(\"./tests/.ssh/test\", \"1234\")\n\tassert.NoError(t, err)\n}\n\nfunc TestRunCommandWithFingerprint(t *testing.T) {\n\t// wrong fingerprint\n\tsshConf := &MakeConfig{\n\t\tServer:      \"localhost\",\n\t\tUser:        \"drone-scp\",\n\t\tPort:        \"22\",\n\t\tKeyPath:     \"./tests/.ssh/id_rsa\",\n\t\tFingerprint: \"wrong\",\n\t}\n\n\toutStr, errStr, isTimeout, err := sshConf.Run(\"whoami\", 10)\n\tassert.Equal(t, \"\", outStr)\n\tassert.Equal(t, \"\", errStr)\n\tassert.False(t, isTimeout)\n\tassert.Error(t, err)\n\n\thostKey, err := getHostPublicKeyFile(\"/etc/ssh/ssh_host_rsa_key.pub\")\n\tassert.NoError(t, err)\n\n\tsshConf = &MakeConfig{\n\t\tServer:      \"localhost\",\n\t\tUser:        \"drone-scp\",\n\t\tPort:        \"22\",\n\t\tKeyPath:     \"./tests/.ssh/id_rsa\",\n\t\tFingerprint: ssh.FingerprintSHA256(hostKey),\n\t}\n\n\toutStr, errStr, isTimeout, err = sshConf.Run(\"whoami\")\n\tassert.Equal(t, \"drone-scp\\n\", outStr)\n\tassert.Equal(t, \"\", errStr)\n\tassert.True(t, isTimeout)\n\tassert.NoError(t, err)\n}\n\nfunc TestPrivateKeyAndPassword(t *testing.T) {\n\t// provide password and ssh private key\n\tssh := &MakeConfig{\n\t\tServer:   \"localhost\",\n\t\tUser:     \"drone-scp\",\n\t\tPort:     \"22\",\n\t\tPassword: \"1234\",\n\t\tKeyPath:  \"./tests/.ssh/id_rsa\",\n\t}\n\n\toutStr, errStr, isTimeout, err := ssh.Run(\"whoami\")\n\tassert.Equal(t, \"drone-scp\\n\", outStr)\n\tassert.Equal(t, \"\", errStr)\n\tassert.True(t, isTimeout)\n\tassert.NoError(t, err)\n\n\t// provide correct password and wrong private key\n\tssh = &MakeConfig{\n\t\tServer:   \"localhost\",\n\t\tUser:     \"drone-scp\",\n\t\tPort:     \"22\",\n\t\tPassword: \"1234\",\n\t\tKeyPath:  \"./tests/.ssh/id_rsa.pub\",\n\t}\n\n\toutStr, errStr, isTimeout, err = ssh.Run(\"whoami\")\n\tassert.Equal(t, \"drone-scp\\n\", outStr)\n\tassert.Equal(t, \"\", errStr)\n\tassert.True(t, isTimeout)\n\tassert.NoError(t, err)\n\n\t// provide wrong password and correct private key\n\tssh = &MakeConfig{\n\t\tServer:   \"localhost\",\n\t\tUser:     \"drone-scp\",\n\t\tPort:     \"22\",\n\t\tPassword: \"123456\",\n\t\tKeyPath:  \"./tests/.ssh/id_rsa\",\n\t}\n\n\toutStr, errStr, isTimeout, err = ssh.Run(\"whoami\")\n\tassert.Equal(t, \"drone-scp\\n\", outStr)\n\tassert.Equal(t, \"\", errStr)\n\tassert.True(t, isTimeout)\n\tassert.NoError(t, err)\n}\n\nfunc TestRunCommand(t *testing.T) {\n\t// wrong key\n\tssh := &MakeConfig{\n\t\tServer:  \"localhost\",\n\t\tUser:    \"drone-scp\",\n\t\tPort:    \"22\",\n\t\tKeyPath: \"./tests/.ssh/id_rsa.pub\",\n\t}\n\n\toutStr, errStr, isTimeout, err := ssh.Run(\"whoami\", 10)\n\tassert.Equal(t, \"\", outStr)\n\tassert.Equal(t, \"\", errStr)\n\tassert.False(t, isTimeout)\n\tassert.Error(t, err)\n\n\tssh = &MakeConfig{\n\t\tServer:  \"localhost\",\n\t\tUser:    \"drone-scp\",\n\t\tPort:    \"22\",\n\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t}\n\n\toutStr, errStr, isTimeout, err = ssh.Run(\"whoami\")\n\tassert.Equal(t, \"drone-scp\\n\", outStr)\n\tassert.Equal(t, \"\", errStr)\n\tassert.True(t, isTimeout)\n\tassert.NoError(t, err)\n\n\t// error message: not found\n\toutStr, errStr, isTimeout, err = ssh.Run(\"whoami1234\")\n\tassert.Equal(t, \"\", outStr)\n\tassert.Equal(t, \"sh: whoami1234: not found\\n\", errStr)\n\tassert.True(t, isTimeout)\n\t// Process exited with status 127\n\tassert.Error(t, err)\n\n\t// error message: Run Command Timeout\n\toutStr, errStr, isTimeout, err = ssh.Run(\"sleep 2\", 1*time.Second)\n\tassert.Equal(t, \"\", outStr)\n\tassert.Equal(t, \"\", errStr)\n\tassert.False(t, isTimeout)\n\tassert.Error(t, err)\n\tassert.Equal(t, \"Run Command Timeout: \"+context.DeadlineExceeded.Error(), err.Error())\n\n\t// test exit code\n\toutStr, errStr, isTimeout, err = ssh.Run(\"exit 1\")\n\tassert.Equal(t, \"\", outStr)\n\tassert.Equal(t, \"\", errStr)\n\tassert.True(t, isTimeout)\n\t// Process exited with status 1\n\tassert.Error(t, err)\n}\n\nfunc TestSCPCommand(t *testing.T) {\n\t// wrong key\n\tssh := &MakeConfig{\n\t\tServer:  \"localhost\",\n\t\tUser:    \"drone-scp\",\n\t\tPort:    \"22\",\n\t\tKeyPath: \"./tests/.ssh/id_rsa.pub\",\n\t}\n\n\terr := ssh.Scp(\"./tests/a.txt\", \"a.txt\")\n\tassert.Error(t, err)\n\n\tssh = &MakeConfig{\n\t\tServer:  \"localhost\",\n\t\tUser:    \"drone-scp\",\n\t\tPort:    \"22\",\n\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t}\n\n\terr = ssh.Scp(\"./tests/a.txt\", \"a.txt\")\n\tassert.NoError(t, err)\n\n\tu, err := user.Lookup(\"drone-scp\")\n\tif err != nil {\n\t\tt.Fatalf(\"Lookup: %v\", err)\n\t}\n\n\t// check file exist\n\tif _, err := os.Stat(path.Join(u.HomeDir, \"a.txt\")); os.IsNotExist(err) {\n\t\tt.Fatalf(\"SCP-error: %v\", err)\n\t}\n}\n\nfunc TestSCPCommandWithKey(t *testing.T) {\n\tssh := &MakeConfig{\n\t\tServer: \"localhost\",\n\t\tUser:   \"drone-scp\",\n\t\tPort:   \"22\",\n\t\tKey: `-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA4e2D/qPN08pzTac+a8ZmlP1ziJOXk45CynMPtva0rtK/RB26\nVbfAF0hIJji7ltvnYnqCU9oFfvEM33cTn7T96+od8ib/Vz25YU8ZbstqtIskPuwC\nbv3K0mAHgsviJyRD7yM+QKTbBQEgbGuW6gtbMKhiYfiIB4Dyj7AdS/fk3v26wDgz\n7SHI5OBqu9bv1KhxQYdFEnU3PAtAqeccgzNpbH3eYLyGzuUxEIJlhpZ/uU2G9ppj\n/cSrONVPiI8Ahi4RrlZjmP5l57/sq1ClGulyLpFcMw68kP5FikyqHpHJHRBNgU57\n1y0Ph33SjBbs0haCIAcmreWEhGe+/OXnJe6VUQIDAQABAoIBAH97emORIm9DaVSD\n7mD6DqA7c5m5Tmpgd6eszU08YC/Vkz9oVuBPUwDQNIX8tT0m0KVs42VVPIyoj874\nbgZMJoucC1G8V5Bur9AMxhkShx9g9A7dNXJTmsKilRpk2TOk7wBdLp9jZoKoZBdJ\njlp6FfaazQjjKD6zsCsMATwAoRCBpBNsmT6QDN0n0bIgY0tE6YGQaDdka0dAv68G\nR0VZrcJ9voT6+f+rgJLoojn2DAu6iXaM99Gv8FK91YCymbQlXXgrk6CyS0IHexN7\nV7a3k767KnRbrkqd3o6JyNun/CrUjQwHs1IQH34tvkWScbseRaFehcAm6mLT93RP\nmuauvMECgYEA9AXGtfDMse0FhvDPZx4mx8x+vcfsLvDHcDLkf/lbyPpu97C27b/z\nia07bu5TAXesUZrWZtKA5KeRE5doQSdTOv1N28BEr8ZwzDJwfn0DPUYUOxsN2iIy\nMheO5A45Ko7bjKJVkZ61Mb1UxtqCTF9mqu9R3PBdJGthWOd+HUvF460CgYEA7QRf\nZ8+vpGA+eSuu29e0xgRKnRzed5zXYpcI4aERc3JzBgO4Z0er9G8l66OWVGdMfpe6\nCBajC5ToIiT8zqoYxXwqJgN+glir4gJe3mm8J703QfArZiQrdk0NTi5bY7+vLLG/\nknTrtpdsKih6r3kjhuPPaAsIwmMxIydFvATKjLUCgYEAh/y4EihRSk5WKC8GxeZt\noiZ58vT4z+fqnMIfyJmD5up48JuQNcokw/LADj/ODiFM7GUnWkGxBrvDA3H67WQm\n49bJjs8E+BfUQFdTjYnJRlpJZ+7Zt1gbNQMf5ENw5CCchTDqEq6pN0DVf8PBnSIF\nKvkXW9KvdV5J76uCAn15mDkCgYA1y8dHzbjlCz9Cy2pt1aDfTPwOew33gi7U3skS\nRTerx29aDyAcuQTLfyrROBkX4TZYiWGdEl5Bc7PYhCKpWawzrsH2TNa7CRtCOh2E\nR+V/84+GNNf04ALJYCXD9/ugQVKmR1XfDRCvKeFQFE38Y/dvV2etCswbKt5tRy2p\nxkCe/QKBgQCkLqafD4S20YHf6WTp3jp/4H/qEy2X2a8gdVVBi1uKkGDXr0n+AoVU\nib4KbP5ovZlrjL++akMQ7V2fHzuQIFWnCkDA5c2ZAqzlM+ZN+HRG7gWur7Bt4XH1\n7XC9wlRna4b3Ln8ew3q1ZcBjXwD4ppbTlmwAfQIaZTGJUgQbdsO9YA==\n-----END RSA PRIVATE KEY-----\n`,\n\t}\n\n\t// source file not found\n\terr := ssh.Scp(\"./tests/test.txt\", \"a.txt\")\n\tassert.Error(t, err)\n\n\t// target file not found ex: appleboy folder not found\n\terr = ssh.Scp(\"./tests/a.txt\", \"/appleboy/a.txt\")\n\tassert.Error(t, err)\n\n\terr = ssh.Scp(\"./tests/a.txt\", \"a.txt\")\n\tassert.NoError(t, err)\n\n\tu, err := user.Lookup(\"drone-scp\")\n\tif err != nil {\n\t\tt.Fatalf(\"Lookup: %v\", err)\n\t}\n\n\t// check file exist\n\tif _, err := os.Stat(path.Join(u.HomeDir, \"a.txt\")); os.IsNotExist(err) {\n\t\tt.Fatalf(\"SCP-error: %v\", err)\n\t}\n}\n\nfunc TestProxyClient(t *testing.T) {\n\tssh := &MakeConfig{\n\t\tServer:   \"localhost\",\n\t\tUser:     \"drone-scp\",\n\t\tPort:     \"22\",\n\t\tPassword: \"1234\",\n\t\tProxy: DefaultConfig{\n\t\t\tUser:     \"drone-scp\",\n\t\t\tServer:   \"localhost\",\n\t\t\tPort:     \"22\",\n\t\t\tPassword: \"123456\",\n\t\t},\n\t}\n\n\t// password of proxy client is incorrect.\n\t// can't connect proxy server\n\tsession, client, err := ssh.Connect()\n\tassert.Nil(t, session)\n\tassert.Nil(t, client)\n\tassert.Error(t, err)\n\n\tssh = &MakeConfig{\n\t\tServer:   \"www.che.ccu.edu.tw\",\n\t\tUser:     \"drone-scp\",\n\t\tPort:     \"228\",\n\t\tPassword: \"123456\",\n\t\tProxy: DefaultConfig{\n\t\t\tUser:    \"drone-scp\",\n\t\t\tServer:  \"localhost\",\n\t\t\tPort:    \"22\",\n\t\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\t},\n\t}\n\n\t// proxy client can't dial to target server\n\tsession, client, err = ssh.Connect()\n\tassert.Nil(t, session)\n\tassert.Nil(t, client)\n\tassert.Error(t, err)\n\n\tssh = &MakeConfig{\n\t\tServer:   \"localhost\",\n\t\tUser:     \"drone-scp\",\n\t\tPort:     \"22\",\n\t\tPassword: \"123456\",\n\t\tProxy: DefaultConfig{\n\t\t\tUser:    \"drone-scp\",\n\t\t\tServer:  \"localhost\",\n\t\t\tPort:    \"22\",\n\t\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\t},\n\t}\n\n\t// proxy client can't create new client connection of target\n\tsession, client, err = ssh.Connect()\n\tassert.Nil(t, session)\n\tassert.Nil(t, client)\n\tassert.Error(t, err)\n\n\tssh = &MakeConfig{\n\t\tUser:    \"drone-scp\",\n\t\tServer:  \"localhost\",\n\t\tPort:    \"22\",\n\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\tProxy: DefaultConfig{\n\t\t\tUser:    \"drone-scp\",\n\t\t\tServer:  \"localhost\",\n\t\t\tPort:    \"22\",\n\t\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\t},\n\t}\n\n\tsession, client, err = ssh.Connect()\n\tassert.NotNil(t, session)\n\tassert.NotNil(t, client)\n\tassert.NoError(t, err)\n}\n\nfunc TestProxyClientSSHCommand(t *testing.T) {\n\tssh := &MakeConfig{\n\t\tUser:    \"drone-scp\",\n\t\tServer:  \"localhost\",\n\t\tPort:    \"22\",\n\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\tProxy: DefaultConfig{\n\t\t\tUser:    \"drone-scp\",\n\t\t\tServer:  \"localhost\",\n\t\t\tPort:    \"22\",\n\t\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\t},\n\t}\n\n\toutStr, errStr, isTimeout, err := ssh.Run(\"whoami\")\n\tassert.Equal(t, \"drone-scp\\n\", outStr)\n\tassert.Equal(t, \"\", errStr)\n\tassert.True(t, isTimeout)\n\tassert.NoError(t, err)\n}\n\nfunc TestSCPCommandWithPassword(t *testing.T) {\n\tssh := &MakeConfig{\n\t\tServer:   \"localhost\",\n\t\tUser:     \"drone-scp\",\n\t\tPort:     \"22\",\n\t\tPassword: \"1234\",\n\t\tTimeout:  60 * time.Second,\n\t}\n\n\terr := ssh.Scp(\"./tests/b.txt\", \"b.txt\")\n\tassert.NoError(t, err)\n\n\tu, err := user.Lookup(\"drone-scp\")\n\tif err != nil {\n\t\tt.Fatalf(\"Lookup: %v\", err)\n\t}\n\n\t// check file exist\n\tif _, err := os.Stat(path.Join(u.HomeDir, \"b.txt\")); os.IsNotExist(err) {\n\t\tt.Fatalf(\"SCP-error: %v\", err)\n\t}\n}\n\nfunc TestWrongRawKey(t *testing.T) {\n\t// wrong key\n\tssh := &MakeConfig{\n\t\tServer: \"localhost\",\n\t\tUser:   \"drone-scp\",\n\t\tPort:   \"22\",\n\t\tKey:    \"appleboy\",\n\t}\n\n\toutStr, errStr, isTimeout, err := ssh.Run(\"whoami\")\n\tassert.Equal(t, \"\", outStr)\n\tassert.Equal(t, \"\", errStr)\n\tassert.False(t, isTimeout)\n\tassert.Error(t, err)\n}\n\nfunc TestExitCode(t *testing.T) {\n\tssh := &MakeConfig{\n\t\tServer:  \"localhost\",\n\t\tUser:    \"drone-scp\",\n\t\tPort:    \"22\",\n\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\tTimeout: 60 * time.Second,\n\t}\n\n\toutStr, errStr, isTimeout, err := ssh.Run(\"set -e;echo 1; mkdir a;mkdir a;echo 2\")\n\tassert.Equal(t, \"1\\n\", outStr)\n\tassert.Equal(t, \"mkdir: can't create directory 'a': File exists\\n\", errStr)\n\tassert.True(t, isTimeout)\n\tassert.Error(t, err)\n}\n\nfunc TestSSHWithPassphrase(t *testing.T) {\n\tssh := &MakeConfig{\n\t\tServer:     \"localhost\",\n\t\tUser:       \"drone-scp\",\n\t\tPort:       \"22\",\n\t\tKeyPath:    \"./tests/.ssh/test\",\n\t\tPassphrase: \"1234\",\n\t\tTimeout:    60 * time.Second,\n\t}\n\n\toutStr, errStr, isTimeout, err := ssh.Run(\"set -e;echo 1; mkdir test1234;mkdir test1234;echo 2\")\n\tassert.Equal(t, \"1\\n\", outStr)\n\tassert.Equal(t, \"mkdir: can't create directory 'test1234': File exists\\n\", errStr)\n\tassert.True(t, isTimeout)\n\tassert.Error(t, err)\n}\n\nfunc TestSCPCommandUseInsecureCipher(t *testing.T) {\n\tssh := &MakeConfig{\n\t\tServer:            \"localhost\",\n\t\tUser:              \"drone-scp\",\n\t\tPort:              \"22\",\n\t\tKeyPath:           \"./tests/.ssh/id_rsa\",\n\t\tUseInsecureCipher: true,\n\t}\n\n\terr := ssh.Scp(\"./tests/a.txt\", \"a.txt\")\n\tassert.NoError(t, err)\n\n\tu, err := user.Lookup(\"drone-scp\")\n\tif err != nil {\n\t\tt.Fatalf(\"Lookup: %v\", err)\n\t}\n\n\t// check file exist\n\tif _, err := os.Stat(path.Join(u.HomeDir, \"a.txt\")); os.IsNotExist(err) {\n\t\tt.Fatalf(\"SCP-error: %v\", err)\n\t}\n}\n\n// TestRootAccount test root account\nfunc TestRootAccount(t *testing.T) {\n\tssh := &MakeConfig{\n\t\tServer:  \"localhost\",\n\t\tUser:    \"root\",\n\t\tPort:    \"22\",\n\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t}\n\n\toutStr, errStr, isTimeout, err := ssh.Run(\"whoami\")\n\tassert.Equal(t, \"root\\n\", outStr)\n\tassert.Equal(t, \"\", errStr)\n\tassert.True(t, isTimeout)\n\tassert.NoError(t, err)\n}\n\n// TestSudoCommand\nfunc TestSudoCommand(t *testing.T) {\n\tssh := &MakeConfig{\n\t\tServer:     \"localhost\",\n\t\tUser:       \"drone-scp\",\n\t\tPort:       \"22\",\n\t\tKeyPath:    \"./tests/.ssh/id_rsa\",\n\t\tRequestPty: true,\n\t}\n\n\toutStr, errStr, isTimeout, err := ssh.Run(`sudo su - -c \"whoami\"`)\n\tassert.Equal(t, \"root\\r\\n\", outStr)\n\tassert.Equal(t, \"\", errStr)\n\tassert.True(t, isTimeout)\n\tassert.NoError(t, err)\n}\n\nfunc TestCommandTimeout(t *testing.T) {\n\tssh := &MakeConfig{\n\t\tServer:  \"localhost\",\n\t\tUser:    \"root\",\n\t\tPort:    \"22\",\n\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t}\n\n\toutStr, errStr, isTimeout, err := ssh.Run(\"whoami; sleep 2\", 1*time.Second)\n\tassert.Equal(t, \"root\\n\", outStr)\n\tassert.Equal(t, \"\", errStr)\n\tassert.False(t, isTimeout)\n\tassert.NotNil(t, err)\n\tassert.Equal(t, \"Run Command Timeout: \"+context.DeadlineExceeded.Error(), err.Error())\n}\n\n// TestProxyTimeoutHandling tests that timeout is properly respected when using proxy connections\n// This test uses a non-existent proxy server to force a timeout during proxy connection\nfunc TestProxyTimeoutHandling(t *testing.T) {\n\tssh := &MakeConfig{\n\t\tServer:  \"example.com\",\n\t\tUser:    \"testuser\",\n\t\tPort:    \"22\",\n\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\tTimeout: 1 * time.Second, // Short timeout for testing\n\t\tProxy: DefaultConfig{\n\t\t\tUser:    \"testuser\",\n\t\t\tServer:  \"10.255.255.1\", // Non-routable IP that should timeout\n\t\t\tPort:    \"22\",\n\t\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\t\tTimeout: 1 * time.Second,\n\t\t},\n\t}\n\n\t// Test Connect() method directly to test proxy connection timeout\n\tstart := time.Now()\n\tsession, client, err := ssh.Connect()\n\telapsed := time.Since(start)\n\n\t// Should timeout within reasonable bounds\n\tassert.True(t, elapsed < 3*time.Second, \"Connection should timeout within 3 seconds, took %v\", elapsed)\n\tassert.True(t, elapsed >= 1*time.Second, \"Connection should take at least 1 second (timeout value), took %v\", elapsed)\n\n\t// Should return nil session and client\n\tassert.Nil(t, session)\n\tassert.Nil(t, client)\n\n\t// Should have error\n\tassert.NotNil(t, err)\n}\n\n// TestProxyDialTimeout tests the specific scenario described in issue #93\n// where proxy dial timeout should be respected and properly detected\nfunc TestProxyDialTimeout(t *testing.T) {\n\tssh := &MakeConfig{\n\t\tServer:  \"10.255.255.1\", // Non-routable IP that should timeout\n\t\tUser:    \"testuser\",\n\t\tPort:    \"22\",\n\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\tTimeout: 2 * time.Second, // Short timeout for testing\n\t\tProxy: DefaultConfig{\n\t\t\tUser:    \"testuser\",\n\t\t\tServer:  \"10.255.255.2\", // Another non-routable IP for proxy\n\t\t\tPort:    \"22\",\n\t\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\t\tTimeout: 2 * time.Second,\n\t\t},\n\t}\n\n\t// Test Connect() method directly to avoid SSH server dependency\n\tstart := time.Now()\n\tsession, client, err := ssh.Connect()\n\telapsed := time.Since(start)\n\n\t// Should timeout within reasonable bounds\n\tassert.True(t, elapsed < 5*time.Second, \"Connection should timeout within 5 seconds, took %v\", elapsed)\n\tassert.True(t, elapsed >= 2*time.Second, \"Connection should take at least 2 seconds (timeout value), took %v\", elapsed)\n\n\t// Should return nil session and client\n\tassert.Nil(t, session)\n\tassert.Nil(t, client)\n\n\t// Should have error\n\tassert.NotNil(t, err)\n\t// Note: This will timeout at the proxy connection level, not at proxy dial level\n\t// so it won't be ErrProxyDialTimeout, but we can still verify the timeout behavior\n}\n\n// TestProxyDialTimeoutInRun tests timeout detection in Run method\nfunc TestProxyDialTimeoutInRun(t *testing.T) {\n\tssh := &MakeConfig{\n\t\tServer:  \"example.com\",\n\t\tUser:    \"testuser\",\n\t\tPort:    \"22\",\n\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\tTimeout: 2 * time.Second,\n\t\tProxy: DefaultConfig{\n\t\t\tUser:    \"testuser\",\n\t\t\tServer:  \"127.0.0.1\", // Assume localhost SSH exists\n\t\t\tPort:    \"22\",\n\t\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\t\tTimeout: 2 * time.Second,\n\t\t},\n\t}\n\n\t// Mock a scenario where Connect() returns ErrProxyDialTimeout\n\t// by temporarily changing the target to a non-routable address\n\tssh.Server = \"10.255.255.1\"\n\n\tstart := time.Now()\n\toutStr, errStr, isTimeout, err := ssh.Run(\"whoami\")\n\telapsed := time.Since(start)\n\n\t// Should timeout within reasonable bounds\n\tassert.True(t, elapsed < 5*time.Second, \"Should timeout within 5 seconds, took %v\", elapsed)\n\n\t// Should return empty output\n\tassert.Equal(t, \"\", outStr)\n\tassert.Equal(t, \"\", errStr)\n\n\t// Should have error\n\tassert.NotNil(t, err)\n\n\t// If it's specifically a proxy dial timeout, isTimeout should be true\n\tif errors.Is(err, ErrProxyDialTimeout) {\n\t\tassert.True(t, isTimeout, \"isTimeout should be true for proxy dial timeout\")\n\t}\n}\n\n// TestProxyGoroutineLeak tests that no goroutines are leaked when proxy dial times out\nfunc TestProxyGoroutineLeak(t *testing.T) {\n\t// Get initial goroutine count\n\tinitialGoroutines := runtime.NumGoroutine()\n\n\tssh := &MakeConfig{\n\t\tServer:  \"10.255.255.1\", // Non-routable IP that should timeout\n\t\tUser:    \"testuser\",\n\t\tPort:    \"22\",\n\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\tTimeout: 1 * time.Second, // Short timeout\n\t\tProxy: DefaultConfig{\n\t\t\tUser:    \"testuser\",\n\t\t\tServer:  \"10.255.255.2\", // Another non-routable IP for proxy\n\t\t\tPort:    \"22\",\n\t\t\tKeyPath: \"./tests/.ssh/id_rsa\",\n\t\t\tTimeout: 1 * time.Second,\n\t\t},\n\t}\n\n\t// Run multiple timeout operations\n\tfor i := 0; i < 5; i++ {\n\t\t_, _, err := ssh.Connect()\n\t\tassert.NotNil(t, err) // Should have error due to timeout\n\t}\n\n\t// Give some time for goroutines to cleanup\n\ttime.Sleep(100 * time.Millisecond)\n\truntime.GC() // Force garbage collection\n\n\t// Check final goroutine count - should not have grown significantly\n\tfinalGoroutines := runtime.NumGoroutine()\n\n\t// Allow for some variance due to test framework overhead, but shouldn't grow by more than 2-3 goroutines\n\tassert.True(t, finalGoroutines <= initialGoroutines+3,\n\t\t\"Goroutine leak detected: initial=%d, final=%d\", initialGoroutines, finalGoroutines)\n}\n\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/appleboy/easyssh-proxy\n\ngo 1.25.9\n\nrequire (\n\tgithub.com/ScaleFT/sshkeys v1.4.0\n\tgithub.com/stretchr/testify v1.8.4\n\tgolang.org/x/crypto v0.43.0\n)\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgolang.org/x/sys v0.37.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/ScaleFT/sshkeys v1.4.0 h1:Yqd0cKA5PUvwV0dgRI67BDHGTsMHtGQBZbLXh1dthmE=\ngithub.com/ScaleFT/sshkeys v1.4.0/go.mod h1:GineMkS8SEiELq8q5DzA2Wnrw65SqdD9a+hm8JOU1I4=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU=\ngithub.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngolang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=\ngolang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=\ngolang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=\ngolang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=\ngolang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "tests/.ssh/id_rsa",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA4e2D/qPN08pzTac+a8ZmlP1ziJOXk45CynMPtva0rtK/RB26\nVbfAF0hIJji7ltvnYnqCU9oFfvEM33cTn7T96+od8ib/Vz25YU8ZbstqtIskPuwC\nbv3K0mAHgsviJyRD7yM+QKTbBQEgbGuW6gtbMKhiYfiIB4Dyj7AdS/fk3v26wDgz\n7SHI5OBqu9bv1KhxQYdFEnU3PAtAqeccgzNpbH3eYLyGzuUxEIJlhpZ/uU2G9ppj\n/cSrONVPiI8Ahi4RrlZjmP5l57/sq1ClGulyLpFcMw68kP5FikyqHpHJHRBNgU57\n1y0Ph33SjBbs0haCIAcmreWEhGe+/OXnJe6VUQIDAQABAoIBAH97emORIm9DaVSD\n7mD6DqA7c5m5Tmpgd6eszU08YC/Vkz9oVuBPUwDQNIX8tT0m0KVs42VVPIyoj874\nbgZMJoucC1G8V5Bur9AMxhkShx9g9A7dNXJTmsKilRpk2TOk7wBdLp9jZoKoZBdJ\njlp6FfaazQjjKD6zsCsMATwAoRCBpBNsmT6QDN0n0bIgY0tE6YGQaDdka0dAv68G\nR0VZrcJ9voT6+f+rgJLoojn2DAu6iXaM99Gv8FK91YCymbQlXXgrk6CyS0IHexN7\nV7a3k767KnRbrkqd3o6JyNun/CrUjQwHs1IQH34tvkWScbseRaFehcAm6mLT93RP\nmuauvMECgYEA9AXGtfDMse0FhvDPZx4mx8x+vcfsLvDHcDLkf/lbyPpu97C27b/z\nia07bu5TAXesUZrWZtKA5KeRE5doQSdTOv1N28BEr8ZwzDJwfn0DPUYUOxsN2iIy\nMheO5A45Ko7bjKJVkZ61Mb1UxtqCTF9mqu9R3PBdJGthWOd+HUvF460CgYEA7QRf\nZ8+vpGA+eSuu29e0xgRKnRzed5zXYpcI4aERc3JzBgO4Z0er9G8l66OWVGdMfpe6\nCBajC5ToIiT8zqoYxXwqJgN+glir4gJe3mm8J703QfArZiQrdk0NTi5bY7+vLLG/\nknTrtpdsKih6r3kjhuPPaAsIwmMxIydFvATKjLUCgYEAh/y4EihRSk5WKC8GxeZt\noiZ58vT4z+fqnMIfyJmD5up48JuQNcokw/LADj/ODiFM7GUnWkGxBrvDA3H67WQm\n49bJjs8E+BfUQFdTjYnJRlpJZ+7Zt1gbNQMf5ENw5CCchTDqEq6pN0DVf8PBnSIF\nKvkXW9KvdV5J76uCAn15mDkCgYA1y8dHzbjlCz9Cy2pt1aDfTPwOew33gi7U3skS\nRTerx29aDyAcuQTLfyrROBkX4TZYiWGdEl5Bc7PYhCKpWawzrsH2TNa7CRtCOh2E\nR+V/84+GNNf04ALJYCXD9/ugQVKmR1XfDRCvKeFQFE38Y/dvV2etCswbKt5tRy2p\nxkCe/QKBgQCkLqafD4S20YHf6WTp3jp/4H/qEy2X2a8gdVVBi1uKkGDXr0n+AoVU\nib4KbP5ovZlrjL++akMQ7V2fHzuQIFWnCkDA5c2ZAqzlM+ZN+HRG7gWur7Bt4XH1\n7XC9wlRna4b3Ln8ew3q1ZcBjXwD4ppbTlmwAfQIaZTGJUgQbdsO9YA==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "tests/.ssh/id_rsa.pub",
    "content": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDh7YP+o83TynNNpz5rxmaU/XOIk5eTjkLKcw+29rSu0r9EHbpVt8AXSEgmOLuW2+dieoJT2gV+8QzfdxOftP3r6h3yJv9XPblhTxluy2q0iyQ+7AJu/crSYAeCy+InJEPvIz5ApNsFASBsa5bqC1swqGJh+IgHgPKPsB1L9+Te/brAODPtIcjk4Gq71u/UqHFBh0USdTc8C0Cp5xyDM2lsfd5gvIbO5TEQgmWGln+5TYb2mmP9xKs41U+IjwCGLhGuVmOY/mXnv+yrUKUa6XIukVwzDryQ/kWKTKoekckdEE2BTnvXLQ+HfdKMFuzSFoIgByat5YSEZ7785ecl7pVR drone-scp@localhost\n"
  },
  {
    "path": "tests/.ssh/test",
    "content": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAgX0UT5U\ndbd5qk/WLiRyDeAAAAEAAAAAEAAAIXAAAAB3NzaC1yc2EAAAADAQABAAACAQDojzlRtxSq\nAaOGaPHwCSRlsw870qwpc55W5AxlcOsbFZdtSwZ/dESBu5ql3dLsTB7WcqXoaA7Qp3w5GV\nRcFxn+5r2dL17MPe3zZrLNZulbnkXiaVgYLjWa0cAv9zD+0nR8/mtz2DbkpKCD8R3oLJ2B\nz5oscT2XLcPvIKZlw2eBErpSopLxfpPyhU8WNK9E38mUl2tjtiBVIoIJmtgYWY8XmIpEUR\niiRjPidBJUVmLq9kKdUV62V/pMB2UDzqPJUiuABgzh8/9/qM81uMCwyqULzaVhPE+S9P7L\ndv1Npj5nqwOmUcGj0dhofi+F+qZ8WqGkQJ5JPam0LkGuMKGNJywrxo8XTXSpYCUvbKrTWo\n0/1GNLCcHpIcjhUUJGObOMk1YI0Tu52PGpzDf1kI+zzAgPqWxxzegQdLcPIgm4I6x6S48E\nV13THoAoU5T+rLrhE0i6FokGIwKv4SycDtFCvIdOr1jxJpw0CrKHqMG/kzeljtM1HOojD5\ngHwESwwsZL5P5/IWvnZlGZD0fAp/SPWpZIeMTeH12QANxX69RoQfKLRMYWSabDUvGKkIxQ\nCBaoVOmyVQIqyT5wTZ3msfGVLb729TlZcNo+8snG0k2W9skdlahx1TugzE096P+RzjUfov\n6g1NZKHCN8VSeu4+gmPIEiuN3tt99dKDNBMx/QYLfPpwAAB1DxxEspVRHEF4mTxw4+hFhe\n855+u3ffHmjgrK7IWZqrrze8bayRAVKPK7UMux4ZCOccc1ydtJFGUrZw8Q5gMe+Y+TusXE\nWB7LWZK5an/WrEVe1jNgxwrQKjXauKtTY33CFnnTvdE8dUixHr3AddYq1gQ4WcB7v08sj6\nf8V4yf80u294H5pjYxFMmTu4QldphV/mZcPQCyuzZKmkPLK2TzZWqGk615zJDd/W8Inm1c\nIJTQPH4tIA3X3daThxOMLC3eQXC2rvl7qaSz2k8ok7LnND8GrTU0CnE6XUMNjRjlxXO+6n\nXGVILifwk+bdLlE6aPIqhSuwx/TnbzHwj8DYnd5/Da/KdXpbc4T+925w6til98lyfRICol\nSo6gXace2IK33LKEAaEIr3im+ZFgSIvWZdPu/ZPV7nlNcb/rbMsRF6fKAMFA/kpPr4z5tQ\n0pMJYfUmPCMdP7ahZ3km/Cpmee/VQ07s11myA2XeaEPov6yNrHJJtnAp5wsZ5s6ifLmoyl\nWEPKt8YoIIDib44ANoyhgf6+PA2i+367p5U55ynI6HTXOdB/xWqJ7k4Rah4BSbd1t73wyU\nkh04/9+YGDabwup7WzT07S7b+T5tGKAwMwK0cE/y5RyVI5JwT2b7fvbO6YH9ZFaNOMT+e0\njBpsrEDkdqnVaFU5b6yWO4Gw6I4Myw+ByATlPM6rmAQoOgfmQmoI47UiTYPr4bLvB7w02B\nyQb2AxWDFdCJadZDTpFp5E5mXudt3JnfnKpR+9zDud4AahEU63ggJn0gpd51zRtqrTViob\nqnZ9UtMl9dzGReZFInS6Uq6ge4JJlsxhEVREr/RPUa4NldT7nRMnfxlnwiVcgrdNdeL2Ho\n5azXYMsDsACBR9rmz6h7JOpM/oyupbyitGtJMiBrR20UD264L4zZIBU4d6MXQADdOo+oKE\nOofB9Zj9ovIAGzb9SNAh7vEXE0X6C/EdRNI/DOlca15bd33r+HgKTIONqTws2wThLnSx1r\nW0voqE6SnhUhQ1FovxtBFYE1Ve8HR7BGyO6AJGQOpqivLry7W1BJyOiwSJ+DDUmcrnCyqJ\nIoDc9pHQ/9hBlJz2qeBNaSdwMZWKkTCnIYq1f8FAKdRqujFx81toUZw7Lb03JmKZBlALvU\nPvXcfxCDqSVy66SHVEtGvegFCeo0gQJS0BywQkushDVSoqGQppVoICNtyuzaMzpzkWj5tP\niYJzm5VBDucWkCmmqzFVVVdeX4Vd5crQ08ZeuHHOAL61bKde7ji1XnvlmllpByeHA5uPef\nmGmgf/A1fM3MqioWW/C/Beffe5tJDKxoG7lavIg7F83c48wk+SeJ+2twNMySu1PDYmlRvb\nVUdy1LtjutFtvySYBDHTFUOkvTOCX01gMTaB8HhaZAA+cjt91jrdKkKBh8spQHg1En/oaR\nrmuqMWuMadxKIlzUBWLkAme78ce0SbpnGmBG1jz6kbjF3ZWWFJVj+3DHwE9dydfX4gCKT8\niV3IuifJLGtmaQO5AgpquMWYKsdOI+HCsWN2+YngqScmfokMSR4bn8DmcLNVYzzNkfo2/o\nOc/ZHtSJlXsY/5el+Bg4FBsvX3akyz32KJ9azsexjMFQl6dt5e6qAeV7kGKic8uaqXhWYW\nl/sKzuqXVqVP8QwfipH+SZB3b71canc2mnC3+eXroSgG9yYneGzxfP4ppABGRu/hSyLaaq\ntGyqgelrCXKSiWQs/Vgj46zEAoeIvLW9/NwnPkV3r499Ieh7kqMl2iZzpBXqab2ip6yt0+\n9GQuBwb3YNj7HO/a2YU0aMJrs6YofOa6/0h4ZvLYe6ndzDAAFIlUmqGiHUnjnDtChaUZLX\nE+9a8GkASVSizvMEpo/71uzOhn3Ta9ixDBqDQgA1DD0p3ko4bq7nYTNIfqghpJW2yTb8Sq\nFw9yuZ7WRcS/SNmsVxCN8UsimixI4uugKgiU+YWLdZrlaQk7yCRUZ4Drris4FBW55AjVJ8\nnz3suOA+nh8JK0DN99hE9EAtgtMc1oKb76te1VCtd5tUfjN13qq5SvHMQp/dn+y+aVSIEg\nKrvhXVwxyncL7AC04yK0TJHVk83vXCK6hyFnPeFBPhY0yUtq1smWLrjotaW8ZRLMKG6Kz/\ncD69lsCnhFATfbBmKh6mRrBUaZV3nvZKKojJGnTguOQZodg0EEx/XR+aXFmJfKzyo4wdfK\nOQR/HeDLS+X1tAzEkZl3QtAgeNWwngXlov3wJgg0R5X4scJZlG9ns7UNrJG2D9E4LTLMvZ\nW1d9tnqAJprUdR9vvqUXGgbndzV+MuV/gY52jt2p7gvscBFVwLLuH7eTarrvqfBPAx+I33\nV79GEkDdc9rCpA6BGDEJGr/Xcpx2tDiSqLc8vELfpruROx4T4PuPZvKqqcvHNNUYUQi1+y\n7quwL7RgZj+i5hXGTRQ5Y+YfVYY+7sNgUxQpS5pC64s7bvwB0pHjgjn1KTXuyroPkV6pWA\nFfFEk1ETJhXcl7plxpmcLROyI=\n-----END OPENSSH PRIVATE KEY-----\n"
  },
  {
    "path": "tests/.ssh/test.pub",
    "content": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDojzlRtxSqAaOGaPHwCSRlsw870qwpc55W5AxlcOsbFZdtSwZ/dESBu5ql3dLsTB7WcqXoaA7Qp3w5GVRcFxn+5r2dL17MPe3zZrLNZulbnkXiaVgYLjWa0cAv9zD+0nR8/mtz2DbkpKCD8R3oLJ2Bz5oscT2XLcPvIKZlw2eBErpSopLxfpPyhU8WNK9E38mUl2tjtiBVIoIJmtgYWY8XmIpEURiiRjPidBJUVmLq9kKdUV62V/pMB2UDzqPJUiuABgzh8/9/qM81uMCwyqULzaVhPE+S9P7Ldv1Npj5nqwOmUcGj0dhofi+F+qZ8WqGkQJ5JPam0LkGuMKGNJywrxo8XTXSpYCUvbKrTWo0/1GNLCcHpIcjhUUJGObOMk1YI0Tu52PGpzDf1kI+zzAgPqWxxzegQdLcPIgm4I6x6S48EV13THoAoU5T+rLrhE0i6FokGIwKv4SycDtFCvIdOr1jxJpw0CrKHqMG/kzeljtM1HOojD5gHwESwwsZL5P5/IWvnZlGZD0fAp/SPWpZIeMTeH12QANxX69RoQfKLRMYWSabDUvGKkIxQCBaoVOmyVQIqyT5wTZ3msfGVLb729TlZcNo+8snG0k2W9skdlahx1TugzE096P+RzjUfov6g1NZKHCN8VSeu4+gmPIEiuN3tt99dKDNBMx/QYLfPpw== deploy@easyssh\n"
  },
  {
    "path": "tests/a.txt",
    "content": "appleboy\n"
  },
  {
    "path": "tests/b.txt",
    "content": ""
  },
  {
    "path": "tests/entrypoint.sh",
    "content": "#!/bin/sh\n\nif [ ! -f \"/etc/ssh/ssh_host_rsa_key\" ]; then\n  # generate fresh rsa key\n  ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N '' -t rsa\nfi\n\nif [ ! -f \"/etc/ssh/ssh_host_dsa_key\" ]; then\n  # generate fresh dsa key\n  ssh-keygen -f /etc/ssh/ssh_host_dsa_key -N '' -t dsa\nfi\n\nexec \"$@\"\n"
  },
  {
    "path": "tests/global/c.txt",
    "content": ""
  },
  {
    "path": "tests/global/d.txt",
    "content": ""
  },
  {
    "path": "tests/sudoers",
    "content": "Defaults        requiretty\ndrone-scp ALL=(ALL) NOPASSWD:ALL\n"
  }
]