mirror of
https://github.com/appleboy/drone-ssh.git
synced 2026-06-16 14:49:25 +08:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9cc37282c | |||
| 6e431b0c53 | |||
| 3499506089 | |||
| 6c0b475c15 | |||
| 60993a71e2 | |||
| 8bfc58f9d0 | |||
| 7f4cb1c1d0 | |||
| f92f762c9d | |||
| 84cb184039 | |||
| 31c084fd3e | |||
| 69b3a40978 | |||
| 4d443c40f2 | |||
| 9dd4b8db8d | |||
| 45f43d7ffd | |||
| 7220c94832 | |||
| 2d5668ff17 | |||
| 6f1ace35bf | |||
| 05ebe5b663 | |||
| e331f975ad | |||
| f943ff7179 | |||
| 65e15c4aab | |||
| 83273b5669 | |||
| a8392b5f22 | |||
| e057a699a4 | |||
| 14fddbbba5 | |||
| 5fbd22f265 | |||
| bf269615ce | |||
| 538a5a6ce5 | |||
| 78f4f15754 | |||
| 40323f23e5 | |||
| ed83305de8 | |||
| 4e625fa760 | |||
| c79b44dca2 | |||
| c86c472904 | |||
| ecfaecd46d | |||
| e6d4fa77d1 | |||
| 9651a4eb6c | |||
| b5b13e8b72 | |||
| 26b3d47ee2 | |||
| 0a78278313 | |||
| a7c37e0936 | |||
| 699d9148d8 | |||
| ceec42efdd | |||
| 88b5394dac | |||
| 1637772e0b | |||
| efdac217bd | |||
| f81056261d | |||
| 3fffe80a14 | |||
| 2d568d1fde | |||
| f26bd7f7f7 | |||
| 95427edbba | |||
| 7f168bd1cb | |||
| b6c973ef1e | |||
| 356b2ae6cc | |||
| b698d56d60 | |||
| 06f4f77ebc | |||
| b63f275e9e | |||
| 4d8adbffca | |||
| c73e22e279 | |||
| 6c2d8f278d | |||
| a4dc098318 | |||
| c2776cbaed | |||
| 05b1a61165 | |||
| d447bbd595 | |||
| 6921b0b786 | |||
| 20a4793249 | |||
| b6ec7c2347 | |||
| e5dc646e5d | |||
| 530df8d98b | |||
| 7e4e0224ee |
+67
-34
@@ -1,63 +1,96 @@
|
|||||||
workspace:
|
workspace:
|
||||||
base: /srv/app
|
base: /go/src
|
||||||
path: src/github.com/appleboy/drone-ssh
|
path: github.com/appleboy/drone-ssh
|
||||||
|
|
||||||
|
clone:
|
||||||
|
git:
|
||||||
|
image: plugins/git
|
||||||
|
depth: 50
|
||||||
|
tags: true
|
||||||
|
|
||||||
pipeline:
|
pipeline:
|
||||||
clone:
|
lint:
|
||||||
image: plugins/git
|
image: golang:1.11
|
||||||
tags: true
|
pull: true
|
||||||
|
group: golang
|
||||||
|
commands:
|
||||||
|
- make vet
|
||||||
|
- make lint
|
||||||
|
- make test-vendor
|
||||||
|
|
||||||
|
linux_amd64:
|
||||||
|
image: golang:1.11
|
||||||
|
pull: true
|
||||||
|
group: golang
|
||||||
|
commands:
|
||||||
|
- make linux_amd64
|
||||||
|
|
||||||
|
linux_arm64:
|
||||||
|
image: golang:1.11
|
||||||
|
pull: true
|
||||||
|
group: golang
|
||||||
|
commands:
|
||||||
|
- make linux_arm64
|
||||||
|
|
||||||
|
linux_arm:
|
||||||
|
image: golang:1.11
|
||||||
|
pull: true
|
||||||
|
group: golang
|
||||||
|
commands:
|
||||||
|
- make linux_arm
|
||||||
|
|
||||||
test:
|
test:
|
||||||
image: appleboy/golang-testing
|
image: appleboy/golang-testing
|
||||||
pull: true
|
pull: true
|
||||||
environment:
|
group: golang
|
||||||
TAGS: netgo
|
|
||||||
GOPATH: /srv/app
|
|
||||||
commands:
|
commands:
|
||||||
- make ssh-server
|
- make ssh-server
|
||||||
- make vet
|
- make test
|
||||||
- make lint
|
|
||||||
# - make test
|
|
||||||
- coverage all
|
|
||||||
- make coverage
|
- make coverage
|
||||||
- make build
|
|
||||||
# build binary for docker image
|
|
||||||
- make static_build
|
|
||||||
when:
|
|
||||||
event: [ push, tag, pull_request ]
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
image: appleboy/golang-testing
|
image: golang:1.11
|
||||||
pull: true
|
pull: true
|
||||||
environment:
|
|
||||||
TAGS: netgo
|
|
||||||
GOPATH: /srv/app
|
|
||||||
commands:
|
commands:
|
||||||
- make release
|
- make release
|
||||||
when:
|
when:
|
||||||
event: [ tag ]
|
event: [ tag ]
|
||||||
branch: [ refs/tags/* ]
|
local: false
|
||||||
|
|
||||||
publish_tag:
|
codecov:
|
||||||
image: plugins/docker
|
image: robertstettner/drone-codecov
|
||||||
repo: ${DRONE_REPO}
|
secrets: [ codecov_token ]
|
||||||
tags: [ '${DRONE_TAG}' ]
|
files:
|
||||||
|
- .cover/coverage.txt
|
||||||
when:
|
when:
|
||||||
event: [ tag ]
|
event: [ push, pull_request ]
|
||||||
branch: [ refs/tags/* ]
|
status: [ success ]
|
||||||
|
|
||||||
publish_latest:
|
publish:
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
|
pull: true
|
||||||
repo: ${DRONE_REPO}
|
repo: ${DRONE_REPO}
|
||||||
tags: [ 'latest' ]
|
default_tags: true
|
||||||
|
secrets: [ docker_username, docker_password ]
|
||||||
|
group: release
|
||||||
when:
|
when:
|
||||||
event: [ push ]
|
event: [ push, tag ]
|
||||||
branch: [ master ]
|
local: false
|
||||||
|
|
||||||
release:
|
release_tag:
|
||||||
image: plugins/github-release
|
image: plugins/github-release
|
||||||
|
pull: true
|
||||||
|
secrets: [ github_release_api_key ]
|
||||||
|
group: release
|
||||||
files:
|
files:
|
||||||
- dist/release/*
|
- dist/release/*
|
||||||
when:
|
when:
|
||||||
event: [ tag ]
|
event: [ tag ]
|
||||||
branch: [ refs/tags/* ]
|
local: false
|
||||||
|
|
||||||
|
discord:
|
||||||
|
image: appleboy/drone-discord
|
||||||
|
pull: true
|
||||||
|
secrets: [ discord_webhook_id, discord_webhook_token ]
|
||||||
|
when:
|
||||||
|
status: [ changed, failure ]
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
eyJhbGciOiJIUzI1NiJ9.d29ya3NwYWNlOgogIGJhc2U6IC9zcnYvYXBwCiAgcGF0aDogc3JjL2dpdGh1Yi5jb20vYXBwbGVib3kvZHJvbmUtc3NoCgpwaXBlbGluZToKICBjbG9uZToKICAgIGltYWdlOiBwbHVnaW5zL2dpdAogICAgdGFnczogdHJ1ZQoKICB0ZXN0OgogICAgaW1hZ2U6IGFwcGxlYm95L2dvbGFuZy10ZXN0aW5nCiAgICBwdWxsOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgVEFHUzogbmV0Z28KICAgICAgR09QQVRIOiAvc3J2L2FwcAogICAgY29tbWFuZHM6CiAgICAgIC0gbWFrZSBzc2gtc2VydmVyCiAgICAgIC0gbWFrZSB2ZXQKICAgICAgLSBtYWtlIGxpbnQKICAgICAgIyAtIG1ha2UgdGVzdAogICAgICAtIGNvdmVyYWdlIGFsbAogICAgICAtIG1ha2UgY292ZXJhZ2UKICAgICAgLSBtYWtlIGJ1aWxkCiAgICAgICMgYnVpbGQgYmluYXJ5IGZvciBkb2NrZXIgaW1hZ2UKICAgICAgLSBtYWtlIHN0YXRpY19idWlsZAogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnLCBwdWxsX3JlcXVlc3QgXQoKICByZWxlYXNlOgogICAgaW1hZ2U6IGFwcGxlYm95L2dvbGFuZy10ZXN0aW5nCiAgICBwdWxsOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgVEFHUzogbmV0Z28KICAgICAgR09QQVRIOiAvc3J2L2FwcAogICAgY29tbWFuZHM6CiAgICAgIC0gbWFrZSByZWxlYXNlCiAgICB3aGVuOgogICAgICBldmVudDogWyB0YWcgXQogICAgICBicmFuY2g6IFsgcmVmcy90YWdzLyogXQoKICBwdWJsaXNoX3RhZzoKICAgIGltYWdlOiBwbHVnaW5zL2RvY2tlcgogICAgcmVwbzogJHtEUk9ORV9SRVBPfQogICAgdGFnczogWyAnJHtEUk9ORV9UQUd9JyBdCiAgICB3aGVuOgogICAgICBldmVudDogWyB0YWcgXQogICAgICBicmFuY2g6IFsgcmVmcy90YWdzLyogXQoKICBwdWJsaXNoX2xhdGVzdDoKICAgIGltYWdlOiBwbHVnaW5zL2RvY2tlcgogICAgcmVwbzogJHtEUk9ORV9SRVBPfQogICAgdGFnczogWyAnbGF0ZXN0JyBdCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoIF0KICAgICAgYnJhbmNoOiBbIG1hc3RlciBdCgogIHJlbGVhc2U6CiAgICBpbWFnZTogcGx1Z2lucy9naXRodWItcmVsZWFzZQogICAgZmlsZXM6CiAgICAgIC0gZGlzdC9yZWxlYXNlLyoKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHRhZyBdCiAgICAgIGJyYW5jaDogWyByZWZzL3RhZ3MvKiBdCg.Vz3qqFJL2stx98NSSdyy7d395NG87d-FPedR8LSGPO8
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# unifying the coding style for different editors and IDEs => editorconfig.org
|
||||||
|
|
||||||
|
; indicate this is the root of the project
|
||||||
|
root = true
|
||||||
|
|
||||||
|
###########################################################
|
||||||
|
; common
|
||||||
|
###########################################################
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
end_of_line = LF
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
###########################################################
|
||||||
|
; make
|
||||||
|
###########################################################
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[makefile]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
###########################################################
|
||||||
|
; markdown
|
||||||
|
###########################################################
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
###########################################################
|
||||||
|
; golang
|
||||||
|
###########################################################
|
||||||
|
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
@@ -25,5 +25,6 @@ _testmain.go
|
|||||||
.env
|
.env
|
||||||
|
|
||||||
coverage.txt
|
coverage.txt
|
||||||
|
release
|
||||||
drone-ssh
|
drone-ssh
|
||||||
.cover
|
.cover
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ pipeline:
|
|||||||
- echo world
|
- echo world
|
||||||
```
|
```
|
||||||
|
|
||||||
Example configuration for login with user private key:
|
Example configuration for command timeout (unit: second), default value is 60 seconds:
|
||||||
|
|
||||||
```diff
|
```diff
|
||||||
pipeline:
|
pipeline:
|
||||||
@@ -48,31 +48,15 @@ pipeline:
|
|||||||
image: appleboy/drone-ssh
|
image: appleboy/drone-ssh
|
||||||
host: foo.com
|
host: foo.com
|
||||||
username: root
|
username: root
|
||||||
- password: 1234
|
password: 1234
|
||||||
+ key: ${DEPLOY_KEY}
|
|
||||||
port: 22
|
port: 22
|
||||||
|
+ command_timeout: 120
|
||||||
script:
|
script:
|
||||||
- echo hello
|
- echo hello
|
||||||
- echo world
|
- echo world
|
||||||
```
|
```
|
||||||
|
|
||||||
Example configuration for login with file path of user private key:
|
Example configuration for execute commands on a remote server using `SSHProxyCommand`:
|
||||||
|
|
||||||
```diff
|
|
||||||
pipeline:
|
|
||||||
ssh:
|
|
||||||
image: appleboy/drone-ssh
|
|
||||||
host: foo.com
|
|
||||||
username: root
|
|
||||||
- password: 1234
|
|
||||||
+ key_path: ./deploy/key.pem
|
|
||||||
port: 22
|
|
||||||
script:
|
|
||||||
- echo hello
|
|
||||||
- echo world
|
|
||||||
```
|
|
||||||
|
|
||||||
Example configuration for success build:
|
|
||||||
|
|
||||||
```diff
|
```diff
|
||||||
pipeline:
|
pipeline:
|
||||||
@@ -85,11 +69,44 @@ pipeline:
|
|||||||
script:
|
script:
|
||||||
- echo hello
|
- echo hello
|
||||||
- echo world
|
- echo world
|
||||||
+ when:
|
+ proxy_host: 10.130.33.145
|
||||||
+ status: success
|
+ proxy_user: ubuntu
|
||||||
|
+ proxy_port: 22
|
||||||
|
+ proxy_password: 1234
|
||||||
```
|
```
|
||||||
|
|
||||||
Example configuration for tag event:
|
Example configuration using password from secrets:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
pipeline:
|
||||||
|
ssh:
|
||||||
|
image: appleboy/drone-ssh
|
||||||
|
host: foo.com
|
||||||
|
username: root
|
||||||
|
- password: 1234
|
||||||
|
port: 22
|
||||||
|
+ secrets: [ ssh_password ]
|
||||||
|
script:
|
||||||
|
- echo hello
|
||||||
|
- echo world
|
||||||
|
```
|
||||||
|
|
||||||
|
Example configuration using ssh key from secrets:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
pipeline:
|
||||||
|
ssh:
|
||||||
|
image: appleboy/drone-ssh
|
||||||
|
host: foo.com
|
||||||
|
username: root
|
||||||
|
port: 22
|
||||||
|
+ secrets: [ ssh_key ]
|
||||||
|
script:
|
||||||
|
- echo hello
|
||||||
|
- echo world
|
||||||
|
```
|
||||||
|
|
||||||
|
Example configuration for exporting custom secrets:
|
||||||
|
|
||||||
```diff
|
```diff
|
||||||
pipeline:
|
pipeline:
|
||||||
@@ -99,14 +116,32 @@ pipeline:
|
|||||||
username: root
|
username: root
|
||||||
password: 1234
|
password: 1234
|
||||||
port: 22
|
port: 22
|
||||||
|
+ secrets: [ aws_access_key_id ]
|
||||||
|
+ envs: [ aws_access_key_id ]
|
||||||
script:
|
script:
|
||||||
- echo hello
|
- export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
|
||||||
- echo world
|
|
||||||
+ when:
|
|
||||||
+ status: success
|
|
||||||
+ event: tag
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Secret Reference
|
||||||
|
|
||||||
|
ssh_username
|
||||||
|
: account for target host user
|
||||||
|
|
||||||
|
ssh_password
|
||||||
|
: password for target host user
|
||||||
|
|
||||||
|
ssh_key
|
||||||
|
: plain text of user private key
|
||||||
|
|
||||||
|
proxy_ssh_username
|
||||||
|
: account for user of proxy server
|
||||||
|
|
||||||
|
proxy_ssh_password
|
||||||
|
: password for user of proxy server
|
||||||
|
|
||||||
|
proxy_ssh_key
|
||||||
|
: plain text of user private key for proxy server
|
||||||
|
|
||||||
# Parameter Reference
|
# Parameter Reference
|
||||||
|
|
||||||
host
|
host
|
||||||
@@ -127,8 +162,32 @@ key
|
|||||||
key_path
|
key_path
|
||||||
: key path of user private key
|
: key path of user private key
|
||||||
|
|
||||||
|
envs
|
||||||
|
: custom secrets which are made available in the script section
|
||||||
|
|
||||||
script
|
script
|
||||||
: execute commands on a remote server
|
: execute commands on a remote server
|
||||||
|
|
||||||
timeout
|
timeout
|
||||||
: Timeout is the maximum amount of time for the TCP connection to establish.
|
: Timeout is the maximum amount of time for the TCP connection to establish.
|
||||||
|
|
||||||
|
command_timeout
|
||||||
|
: Command timeout is the maximum amount of time for the execute commands, default is 60 secs.
|
||||||
|
|
||||||
|
proxy_host
|
||||||
|
: proxy hostname or IP
|
||||||
|
|
||||||
|
proxy_port
|
||||||
|
: ssh port of proxy host
|
||||||
|
|
||||||
|
proxy_username
|
||||||
|
: account for proxy host user
|
||||||
|
|
||||||
|
proxy_password
|
||||||
|
: password for proxy host user
|
||||||
|
|
||||||
|
proxy_key
|
||||||
|
: plain text of proxy private key
|
||||||
|
|
||||||
|
proxy_key_path
|
||||||
|
: key path of proxy private key
|
||||||
|
|||||||
+10
-4
@@ -1,10 +1,16 @@
|
|||||||
FROM alpine:3.4
|
FROM alpine:3.4
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add \
|
apk add -U --no-cache \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
openssh-client && \
|
openssh-client && \
|
||||||
rm -rf /var/cache/apk/*
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
ADD drone-ssh /bin/
|
LABEL org.label-schema.version=latest
|
||||||
|
LABEL org.label-schema.vcs-url="https://github.com/appleboy/drone-ssh.git"
|
||||||
|
LABEL org.label-schema.name="drone-ssh"
|
||||||
|
LABEL org.label-schema.vendor="Bo-Yi Wu"
|
||||||
|
LABEL org.label-schema.schema-version="1.0"
|
||||||
|
|
||||||
|
ADD release/linux/amd64/drone-ssh /bin/
|
||||||
ENTRYPOINT ["/bin/drone-ssh"]
|
ENTRYPOINT ["/bin/drone-ssh"]
|
||||||
|
|||||||
@@ -2,16 +2,20 @@
|
|||||||
|
|
||||||
DIST := dist
|
DIST := dist
|
||||||
EXECUTABLE := drone-ssh
|
EXECUTABLE := drone-ssh
|
||||||
|
GO ?= go
|
||||||
|
|
||||||
# for dockerhub
|
# for dockerhub
|
||||||
DEPLOY_ACCOUNT := appleboy
|
DEPLOY_ACCOUNT := appleboy
|
||||||
DEPLOY_IMAGE := $(EXECUTABLE)
|
DEPLOY_IMAGE := $(EXECUTABLE)
|
||||||
|
GOFMT ?= gofmt "-s"
|
||||||
|
|
||||||
TARGETS ?= linux darwin windows
|
TARGETS ?= linux darwin windows
|
||||||
PACKAGES ?= $(shell go list ./... | grep -v /vendor/)
|
PACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/)
|
||||||
|
GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*")
|
||||||
SOURCES ?= $(shell find . -name "*.go" -type f)
|
SOURCES ?= $(shell find . -name "*.go" -type f)
|
||||||
TAGS ?=
|
TAGS ?=
|
||||||
LDFLAGS ?= -X 'main.Version=$(VERSION)'
|
LDFLAGS ?= -X 'main.Version=$(VERSION)' -X 'main.build=$(NUMBER)'
|
||||||
|
TMPDIR := $(shell mktemp -d 2>/dev/null || mktemp -d -t 'tempdir')
|
||||||
|
|
||||||
ifneq ($(shell uname), Darwin)
|
ifneq ($(shell uname), Darwin)
|
||||||
EXTLDFLAGS = -extldflags "-static" $(null)
|
EXTLDFLAGS = -extldflags "-static" $(null)
|
||||||
@@ -21,49 +25,75 @@ endif
|
|||||||
|
|
||||||
ifneq ($(DRONE_TAG),)
|
ifneq ($(DRONE_TAG),)
|
||||||
VERSION ?= $(DRONE_TAG)
|
VERSION ?= $(DRONE_TAG)
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifneq ($(DRONE_BUILD_NUMBER),)
|
||||||
|
NUMBER ?= $(DRONE_BUILD_NUMBER)
|
||||||
else
|
else
|
||||||
VERSION ?= $(shell git describe --tags --always || git rev-parse --short HEAD)
|
VERSION ?= $(shell git describe --tags --always || git rev-parse --short HEAD)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
|
.PHONY: fmt-check
|
||||||
|
fmt-check:
|
||||||
|
@diff=$$($(GOFMT) -d $(GOFILES)); \
|
||||||
|
if [ -n "$$diff" ]; then \
|
||||||
|
echo "Please run 'make fmt' and commit the result:"; \
|
||||||
|
echo "$${diff}"; \
|
||||||
|
exit 1; \
|
||||||
|
fi;
|
||||||
|
|
||||||
|
.PHONY: test-vendor
|
||||||
|
test-vendor:
|
||||||
|
@hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
|
$(GO) get -u github.com/kardianos/govendor; \
|
||||||
|
fi
|
||||||
|
govendor list +unused | tee "$(TMPDIR)/wc-gitea-unused"
|
||||||
|
[ $$(cat "$(TMPDIR)/wc-gitea-unused" | wc -l) -eq 0 ] || echo "Warning: /!\\ Some vendor are not used /!\\"
|
||||||
|
|
||||||
|
govendor list +outside | tee "$(TMPDIR)/wc-gitea-outside"
|
||||||
|
[ $$(cat "$(TMPDIR)/wc-gitea-outside" | wc -l) -eq 0 ] || exit 1
|
||||||
|
|
||||||
|
govendor status || exit 1
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
find . -name "*.go" -type f -not -path "./vendor/*" | xargs gofmt -s -w
|
$(GOFMT) -w $(GOFILES)
|
||||||
|
|
||||||
vet:
|
vet:
|
||||||
go vet $(PACKAGES)
|
$(GO) vet $(PACKAGES)
|
||||||
|
|
||||||
errcheck:
|
errcheck:
|
||||||
@hash errcheck > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
@hash errcheck > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
go get -u github.com/kisielk/errcheck; \
|
$(GO) get -u github.com/kisielk/errcheck; \
|
||||||
fi
|
fi
|
||||||
errcheck $(PACKAGES)
|
errcheck $(PACKAGES)
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
go get -u github.com/golang/lint/golint; \
|
$(GO) get -u github.com/golang/lint/golint; \
|
||||||
fi
|
fi
|
||||||
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
|
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
|
||||||
|
|
||||||
unconvert:
|
unconvert:
|
||||||
@hash unconvert > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
@hash unconvert > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
go get -u github.com/mdempsky/unconvert; \
|
$(GO) get -u github.com/mdempsky/unconvert; \
|
||||||
fi
|
fi
|
||||||
for PKG in $(PACKAGES); do unconvert -v $$PKG || exit 1; done;
|
for PKG in $(PACKAGES); do unconvert -v $$PKG || exit 1; done;
|
||||||
|
|
||||||
test:
|
test: fmt-check
|
||||||
for PKG in $(PACKAGES); do go test -v -cover -coverprofile $$GOPATH/src/$$PKG/coverage.txt $$PKG || exit 1; done;
|
for PKG in $(PACKAGES); do $(GO) test -v -cover -coverprofile $$GOPATH/src/$$PKG/coverage.txt $$PKG || exit 1; done;
|
||||||
|
|
||||||
html:
|
html:
|
||||||
go tool cover -html=coverage.txt
|
$(GO) tool cover -html=coverage.txt
|
||||||
|
|
||||||
install: $(SOURCES)
|
install: $(SOURCES)
|
||||||
go install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
|
$(GO) install -v -tags '$(TAGS)' -ldflags "$(EXTLDFLAGS)-s -w $(LDFLAGS)"
|
||||||
|
|
||||||
build: $(EXECUTABLE)
|
build: $(EXECUTABLE)
|
||||||
|
|
||||||
$(EXECUTABLE): $(SOURCES)
|
$(EXECUTABLE): $(SOURCES)
|
||||||
go build -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $@
|
$(GO) build -v -tags '$(TAGS)' -ldflags "$(EXTLDFLAGS)-s -w $(LDFLAGS)" -o $@
|
||||||
|
|
||||||
release: release-dirs release-build release-copy release-check
|
release: release-dirs release-build release-copy release-check
|
||||||
|
|
||||||
@@ -72,7 +102,7 @@ release-dirs:
|
|||||||
|
|
||||||
release-build:
|
release-build:
|
||||||
@hash gox > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
@hash gox > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
go get -u github.com/mitchellh/gox; \
|
$(GO) get -u github.com/mitchellh/gox; \
|
||||||
fi
|
fi
|
||||||
gox -os="$(TARGETS)" -arch="amd64 386" -tags="$(TAGS)" -ldflags="-s -w $(LDFLAGS)" -output="$(DIST)/binaries/$(EXECUTABLE)-$(VERSION)-{{.OS}}-{{.Arch}}"
|
gox -os="$(TARGETS)" -arch="amd64 386" -tags="$(TAGS)" -ldflags="-s -w $(LDFLAGS)" -output="$(DIST)/binaries/$(EXECUTABLE)-$(VERSION)-{{.OS}}-{{.Arch}}"
|
||||||
|
|
||||||
@@ -82,15 +112,18 @@ release-copy:
|
|||||||
release-check:
|
release-check:
|
||||||
cd $(DIST)/release; $(foreach file,$(wildcard $(DIST)/release/$(EXECUTABLE)-*),sha256sum $(notdir $(file)) > $(notdir $(file)).sha256;)
|
cd $(DIST)/release; $(foreach file,$(wildcard $(DIST)/release/$(EXECUTABLE)-*),sha256sum $(notdir $(file)) > $(notdir $(file)).sha256;)
|
||||||
|
|
||||||
# for docker.
|
linux_amd64:
|
||||||
static_build:
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -a -tags '$(TAGS)' -ldflags "$(EXTLDFLAGS)-s -w $(LDFLAGS)" -o release/linux/amd64/$(EXECUTABLE)
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)' -o $(DEPLOY_IMAGE)
|
|
||||||
|
linux_arm64:
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -a -tags '$(TAGS)' -ldflags "$(EXTLDFLAGS)-s -w $(LDFLAGS)" -o release/linux/arm64/$(EXECUTABLE)
|
||||||
|
|
||||||
|
linux_arm:
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 $(GO) build -a -tags '$(TAGS)' -ldflags "$(EXTLDFLAGS)-s -w $(LDFLAGS)" -o release/arm/amd64/$(EXECUTABLE)
|
||||||
|
|
||||||
docker_image:
|
docker_image:
|
||||||
docker build -t $(DEPLOY_ACCOUNT)/$(DEPLOY_IMAGE) .
|
docker build -t $(DEPLOY_ACCOUNT)/$(DEPLOY_IMAGE) .
|
||||||
|
|
||||||
docker: static_build docker_image
|
|
||||||
|
|
||||||
docker_deploy:
|
docker_deploy:
|
||||||
ifeq ($(tag),)
|
ifeq ($(tag),)
|
||||||
@echo "Usage: make $@ tag=<tag>"
|
@echo "Usage: make $@ tag=<tag>"
|
||||||
@@ -101,13 +134,10 @@ endif
|
|||||||
docker push $(DEPLOY_ACCOUNT)/$(DEPLOY_IMAGE):$(tag)
|
docker push $(DEPLOY_ACCOUNT)/$(DEPLOY_IMAGE):$(tag)
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
sed -i '/main.go/d' .cover/coverage.txt
|
sed -i '/main.go/d' coverage.txt
|
||||||
curl -s https://codecov.io/bash > .codecov && \
|
|
||||||
chmod +x .codecov && \
|
|
||||||
./.codecov -f .cover/coverage.txt
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
go clean -x -i ./...
|
$(GO) clean -x -i ./...
|
||||||
rm -rf coverage.txt $(EXECUTABLE) $(DIST) vendor
|
rm -rf coverage.txt $(EXECUTABLE) $(DIST) vendor
|
||||||
|
|
||||||
ssh-server:
|
ssh-server:
|
||||||
@@ -122,5 +152,10 @@ ssh-server:
|
|||||||
rm -rf /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_dsa_key
|
rm -rf /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_dsa_key
|
||||||
./tests/entrypoint.sh /usr/sbin/sshd -D &
|
./tests/entrypoint.sh /usr/sbin/sshd -D &
|
||||||
|
|
||||||
|
# Show source statistics.
|
||||||
|
cloc:
|
||||||
|
@cloc -exclude-dir=vendor,node_modules .
|
||||||
|
.PHONY: cloc
|
||||||
|
|
||||||
version:
|
version:
|
||||||
@echo $(VERSION)
|
@echo $(VERSION)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ information and a listing of the available options please take a look at [the do
|
|||||||
|
|
||||||
**Note: Please update your image config path to `appleboy/drone-ssh` for drone. `plugins/ssh` is no longer maintained.**
|
**Note: Please update your image config path to `appleboy/drone-ssh` for drone. `plugins/ssh` is no longer maintained.**
|
||||||
|
|
||||||
|

|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
Build the binary with the following commands:
|
Build the binary with the following commands:
|
||||||
@@ -49,3 +50,26 @@ docker run --rm \
|
|||||||
-w $(pwd) \
|
-w $(pwd) \
|
||||||
appleboy/drone-ssh
|
appleboy/drone-ssh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Mount key from file path
|
||||||
|
|
||||||
|
Please make sure that enable the `trusted` mode in project setting.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Mount private key in `volumes` setting of `.drone.yml` config
|
||||||
|
|
||||||
|
```diff
|
||||||
|
pipeline:
|
||||||
|
ssh:
|
||||||
|
image: appleboy/drone-ssh
|
||||||
|
host: xxxxx.com
|
||||||
|
username: deploy
|
||||||
|
+ volumes:
|
||||||
|
+ - /root/drone_rsa:/root/ssh/drone_rsa
|
||||||
|
key_path: /root/ssh/drone_rsa
|
||||||
|
script:
|
||||||
|
- echo "test ssh"
|
||||||
|
```
|
||||||
|
|
||||||
|
See the detail of [issue comment](https://github.com/appleboy/drone-ssh/issues/51#issuecomment-336732928).
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
// Package easyssh provides a simple implementation of some SSH protocol
|
|
||||||
// features in Go. You can simply run a command on a remote server or get a file
|
|
||||||
// even simpler than native console SSH client. You don't need to think about
|
|
||||||
// Dials, sessions, defers, or public keys... Let easyssh think about it!
|
|
||||||
package easyssh
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MakeConfig Contains main authority information.
|
|
||||||
// User field should be a name of user on remote server (ex. john in ssh john@example.com).
|
|
||||||
// Server field should be a remote machine address (ex. example.com in ssh john@example.com)
|
|
||||||
// Key is a path to private key on your local machine.
|
|
||||||
// Port is SSH server port on remote machine.
|
|
||||||
// Note: easyssh looking for private key in user's home directory (ex. /home/john + Key).
|
|
||||||
// Then ensure your Key begins from '/' (ex. /.ssh/id_rsa)
|
|
||||||
type MakeConfig struct {
|
|
||||||
User string
|
|
||||||
Server string
|
|
||||||
Key string
|
|
||||||
KeyPath string
|
|
||||||
Port string
|
|
||||||
Password string
|
|
||||||
Timeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns ssh.Signer from user you running app home path + cutted key path.
|
|
||||||
// (ex. pubkey,err := getKeyFile("/.ssh/id_rsa") )
|
|
||||||
func getKeyFile(keypath string) (ssh.Signer, error) {
|
|
||||||
buf, err := ioutil.ReadFile(keypath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pubkey, err := ssh.ParsePrivateKey(buf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return pubkey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// connects to remote server using MakeConfig struct and returns *ssh.Session
|
|
||||||
func (ssh_conf *MakeConfig) connect() (*ssh.Session, error) {
|
|
||||||
// auths holds the detected ssh auth methods
|
|
||||||
auths := []ssh.AuthMethod{}
|
|
||||||
|
|
||||||
// figure out what auths are requested, what is supported
|
|
||||||
if ssh_conf.Password != "" {
|
|
||||||
auths = append(auths, ssh.Password(ssh_conf.Password))
|
|
||||||
}
|
|
||||||
|
|
||||||
if ssh_conf.KeyPath != "" {
|
|
||||||
if pubkey, err := getKeyFile(ssh_conf.KeyPath); err == nil {
|
|
||||||
auths = append(auths, ssh.PublicKeys(pubkey))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ssh_conf.Key != "" {
|
|
||||||
signer, _ := ssh.ParsePrivateKey([]byte(ssh_conf.Key))
|
|
||||||
auths = append(auths, ssh.PublicKeys(signer))
|
|
||||||
}
|
|
||||||
|
|
||||||
config := &ssh.ClientConfig{
|
|
||||||
Timeout: ssh_conf.Timeout,
|
|
||||||
User: ssh_conf.User,
|
|
||||||
Auth: auths,
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := ssh.Dial("tcp", net.JoinHostPort(ssh_conf.Server, ssh_conf.Port), config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
session, err := client.NewSession()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return session, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream returns one channel that combines the stdout and stderr of the command
|
|
||||||
// as it is run on the remote machine, and another that sends true when the
|
|
||||||
// command is done. The sessions and channels will then be closed.
|
|
||||||
func (ssh_conf *MakeConfig) Stream(command string) (output chan string, done chan bool, err error) {
|
|
||||||
// connect to remote host
|
|
||||||
session, err := ssh_conf.connect()
|
|
||||||
if err != nil {
|
|
||||||
return output, done, err
|
|
||||||
}
|
|
||||||
// connect to both outputs (they are of type io.Reader)
|
|
||||||
outReader, err := session.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
return output, done, err
|
|
||||||
}
|
|
||||||
errReader, err := session.StderrPipe()
|
|
||||||
if err != nil {
|
|
||||||
return output, done, err
|
|
||||||
}
|
|
||||||
// combine outputs, create a line-by-line scanner
|
|
||||||
outputReader := io.MultiReader(outReader, errReader)
|
|
||||||
err = session.Start(command)
|
|
||||||
scanner := bufio.NewScanner(outputReader)
|
|
||||||
// continuously send the command's output over the channel
|
|
||||||
outputChan := make(chan string)
|
|
||||||
done = make(chan bool)
|
|
||||||
go func(scanner *bufio.Scanner, out chan string, done chan bool) {
|
|
||||||
defer close(outputChan)
|
|
||||||
defer close(done)
|
|
||||||
for scanner.Scan() {
|
|
||||||
outputChan <- scanner.Text()
|
|
||||||
}
|
|
||||||
// close all of our open resources
|
|
||||||
done <- true
|
|
||||||
session.Close()
|
|
||||||
}(scanner, outputChan, done)
|
|
||||||
return outputChan, done, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run command on remote machine and returns its stdout as a string
|
|
||||||
func (ssh_conf *MakeConfig) Run(command string) (outStr string, err error) {
|
|
||||||
outChan, doneChan, err := ssh_conf.Stream(command)
|
|
||||||
if err != nil {
|
|
||||||
return outStr, err
|
|
||||||
}
|
|
||||||
// read from the output channel until the done signal is passed
|
|
||||||
stillGoing := true
|
|
||||||
for stillGoing {
|
|
||||||
select {
|
|
||||||
case <-doneChan:
|
|
||||||
stillGoing = false
|
|
||||||
case line := <-outChan:
|
|
||||||
outStr += line + "\n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// return the concatenation of all signals from the output channel
|
|
||||||
return outStr, err
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,31 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/appleboy/easyssh-proxy"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// build number set at compile-time
|
||||||
|
var build = "0"
|
||||||
|
|
||||||
// Version set at compile-time
|
// Version set at compile-time
|
||||||
var Version = "v1.1.0-dev"
|
var Version string
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
if Version == "" {
|
||||||
|
Version = fmt.Sprintf("1.3.1+%s", build)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load env-file if it exists first
|
||||||
|
if filename, found := os.LookupEnv("PLUGIN_ENV_FILE"); found {
|
||||||
|
_ = godotenv.Load(filename)
|
||||||
|
}
|
||||||
|
|
||||||
app := cli.NewApp()
|
app := cli.NewApp()
|
||||||
app.Name = "Drone SSH"
|
app.Name = "Drone SSH"
|
||||||
app.Usage = "Executing remote ssh commands"
|
app.Usage = "Executing remote ssh commands"
|
||||||
@@ -57,19 +71,83 @@ func main() {
|
|||||||
EnvVar: "PLUGIN_PORT,SSH_PORT",
|
EnvVar: "PLUGIN_PORT,SSH_PORT",
|
||||||
Value: 22,
|
Value: 22,
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "sync",
|
||||||
|
Usage: "sync mode",
|
||||||
|
EnvVar: "PLUGIN_SYNC",
|
||||||
|
},
|
||||||
cli.DurationFlag{
|
cli.DurationFlag{
|
||||||
Name: "timeout,t",
|
Name: "timeout,t",
|
||||||
Usage: "connection timeout",
|
Usage: "connection timeout",
|
||||||
EnvVar: "PLUGIN_TIMEOUT,SSH_TIMEOUT",
|
EnvVar: "PLUGIN_TIMEOUT,SSH_TIMEOUT",
|
||||||
},
|
},
|
||||||
|
cli.IntFlag{
|
||||||
|
Name: "command.timeout,T",
|
||||||
|
Usage: "command timeout",
|
||||||
|
EnvVar: "PLUGIN_COMMAND_TIMEOUT,SSH_COMMAND_TIMEOUT",
|
||||||
|
Value: 60,
|
||||||
|
},
|
||||||
cli.StringSliceFlag{
|
cli.StringSliceFlag{
|
||||||
Name: "script,s",
|
Name: "script,s",
|
||||||
Usage: "execute commands",
|
Usage: "execute commands",
|
||||||
EnvVar: "PLUGIN_SCRIPT,SSH_SCRIPT",
|
EnvVar: "PLUGIN_SCRIPT,SSH_SCRIPT",
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "script.stop",
|
||||||
|
Usage: "stop script after first failure",
|
||||||
|
EnvVar: "PLUGIN_SCRIPT_STOP",
|
||||||
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "env-file",
|
Name: "proxy.ssh-key",
|
||||||
Usage: "source env file",
|
Usage: "private ssh key of proxy",
|
||||||
|
EnvVar: "PLUGIN_PROXY_SSH_KEY,PLUGIN_PROXY_KEY,PROXY_SSH_KEY",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "proxy.key-path",
|
||||||
|
Usage: "ssh private key path of proxy",
|
||||||
|
EnvVar: "PLUGIN_PROXY_KEY_PATH,PROXY_SSH_KEY_PATH",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "proxy.username",
|
||||||
|
Usage: "connect as user of proxy",
|
||||||
|
EnvVar: "PLUGIN_PROXY_USERNAME,PLUGIN_PROXY_USER,PROXY_SSH_USERNAME",
|
||||||
|
Value: "root",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "proxy.password",
|
||||||
|
Usage: "user password of proxy",
|
||||||
|
EnvVar: "PLUGIN_PROXY_PASSWORD,PROXY_SSH_PASSWORD",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "proxy.host",
|
||||||
|
Usage: "connect to host of proxy",
|
||||||
|
EnvVar: "PLUGIN_PROXY_HOST,PROXY_SSH_HOST",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "proxy.port",
|
||||||
|
Usage: "connect to port of proxy",
|
||||||
|
EnvVar: "PLUGIN_PROXY_PORT,PROXY_SSH_PORT",
|
||||||
|
Value: "22",
|
||||||
|
},
|
||||||
|
cli.DurationFlag{
|
||||||
|
Name: "proxy.timeout",
|
||||||
|
Usage: "proxy connection timeout",
|
||||||
|
EnvVar: "PLUGIN_PROXY_TIMEOUT,PROXY_SSH_TIMEOUT",
|
||||||
|
},
|
||||||
|
cli.StringSliceFlag{
|
||||||
|
Name: "secrets",
|
||||||
|
Usage: "plugin secret",
|
||||||
|
EnvVar: "PLUGIN_SECRETS",
|
||||||
|
},
|
||||||
|
cli.StringSliceFlag{
|
||||||
|
Name: "envs",
|
||||||
|
Usage: "Pass envs",
|
||||||
|
EnvVar: "PLUGIN_ENVS",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "debug",
|
||||||
|
Usage: "debug mode",
|
||||||
|
EnvVar: "PLUGIN_DEBUG",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,25 +184,40 @@ REPOSITORY:
|
|||||||
Github: https://github.com/appleboy/drone-ssh
|
Github: https://github.com/appleboy/drone-ssh
|
||||||
`
|
`
|
||||||
|
|
||||||
app.Run(os.Args)
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
fmt.Println("drone-ssh error: ", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(c *cli.Context) error {
|
func run(c *cli.Context) error {
|
||||||
if c.String("env-file") != "" {
|
|
||||||
_ = godotenv.Load(c.String("env-file"))
|
|
||||||
}
|
|
||||||
|
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
Config: Config{
|
Config: Config{
|
||||||
Key: c.String("ssh-key"),
|
Key: c.String("ssh-key"),
|
||||||
KeyPath: c.String("key-path"),
|
KeyPath: c.String("key-path"),
|
||||||
UserName: c.String("user"),
|
UserName: c.String("user"),
|
||||||
Password: c.String("password"),
|
Password: c.String("password"),
|
||||||
Host: c.StringSlice("host"),
|
Host: c.StringSlice("host"),
|
||||||
Port: c.Int("port"),
|
Port: c.Int("port"),
|
||||||
Timeout: c.Duration("timeout"),
|
Timeout: c.Duration("timeout"),
|
||||||
Script: c.StringSlice("script"),
|
CommandTimeout: c.Int("command.timeout"),
|
||||||
|
Script: c.StringSlice("script"),
|
||||||
|
ScriptStop: c.Bool("script.stop"),
|
||||||
|
Secrets: c.StringSlice("secrets"),
|
||||||
|
Envs: c.StringSlice("envs"),
|
||||||
|
Debug: c.Bool("debug"),
|
||||||
|
Sync: c.Bool("sync"),
|
||||||
|
Proxy: easyssh.DefaultConfig{
|
||||||
|
Key: c.String("proxy.ssh-key"),
|
||||||
|
KeyPath: c.String("proxy.key-path"),
|
||||||
|
User: c.String("proxy.username"),
|
||||||
|
Password: c.String("proxy.password"),
|
||||||
|
Server: c.String("proxy.host"),
|
||||||
|
Port: c.String("proxy.port"),
|
||||||
|
Timeout: c.Duration("proxy.timeout"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
Writer: os.Stdout,
|
||||||
}
|
}
|
||||||
|
|
||||||
return plugin.Exec()
|
return plugin.Exec()
|
||||||
|
|||||||
@@ -2,81 +2,163 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"io"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/appleboy/drone-ssh/easyssh"
|
"github.com/appleboy/easyssh-proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
missingHostOrUser = "Error: missing server host or user"
|
missingHostOrUser = "Error: missing server host or user"
|
||||||
missingPasswordOrKey = "Error: can't connect without a private SSH key or password"
|
missingPasswordOrKey = "Error: can't connect without a private SSH key or password"
|
||||||
|
commandTimeOut = "Error: command timeout"
|
||||||
|
setPasswordandKey = "can't set password and key at the same time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// Config for the plugin.
|
// Config for the plugin.
|
||||||
Config struct {
|
Config struct {
|
||||||
Key string
|
Key string
|
||||||
KeyPath string
|
KeyPath string
|
||||||
UserName string
|
UserName string
|
||||||
Password string
|
Password string
|
||||||
Host []string
|
Host []string
|
||||||
Port int
|
Port int
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
Script []string
|
CommandTimeout int
|
||||||
|
Script []string
|
||||||
|
ScriptStop bool
|
||||||
|
Secrets []string
|
||||||
|
Envs []string
|
||||||
|
Proxy easyssh.DefaultConfig
|
||||||
|
Debug bool
|
||||||
|
Sync bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plugin structure
|
// Plugin structure
|
||||||
Plugin struct {
|
Plugin struct {
|
||||||
Config Config
|
Config Config
|
||||||
|
Writer io.Writer
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func escapeArg(arg string) string {
|
||||||
|
return "'" + strings.Replace(arg, "'", `'\''`, -1) + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Plugin) exec(host string, wg *sync.WaitGroup, errChannel chan error) {
|
||||||
|
// Create MakeConfig instance with remote username, server address and path to private key.
|
||||||
|
ssh := &easyssh.MakeConfig{
|
||||||
|
Server: host,
|
||||||
|
User: p.Config.UserName,
|
||||||
|
Password: p.Config.Password,
|
||||||
|
Port: strconv.Itoa(p.Config.Port),
|
||||||
|
Key: p.Config.Key,
|
||||||
|
KeyPath: p.Config.KeyPath,
|
||||||
|
Timeout: p.Config.Timeout,
|
||||||
|
Proxy: easyssh.DefaultConfig{
|
||||||
|
Server: p.Config.Proxy.Server,
|
||||||
|
User: p.Config.Proxy.User,
|
||||||
|
Password: p.Config.Proxy.Password,
|
||||||
|
Port: p.Config.Proxy.Port,
|
||||||
|
Key: p.Config.Proxy.Key,
|
||||||
|
KeyPath: p.Config.Proxy.KeyPath,
|
||||||
|
Timeout: p.Config.Proxy.Timeout,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
p.log(host, "======CMD======")
|
||||||
|
p.log(host, strings.Join(p.Config.Script, "\n"))
|
||||||
|
p.log(host, "======END======")
|
||||||
|
|
||||||
|
env := []string{}
|
||||||
|
for _, key := range p.Config.Envs {
|
||||||
|
key = strings.ToUpper(key)
|
||||||
|
if val, found := os.LookupEnv(key); found {
|
||||||
|
env = append(env, key+"="+escapeArg(val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Config.Script = append(env, p.scriptCommands()...)
|
||||||
|
|
||||||
|
if p.Config.Debug {
|
||||||
|
p.log(host, "======ENV======")
|
||||||
|
p.log(host, strings.Join(env, "\n"))
|
||||||
|
p.log(host, "======END======")
|
||||||
|
}
|
||||||
|
|
||||||
|
stdoutChan, stderrChan, doneChan, errChan, err := ssh.Stream(strings.Join(p.Config.Script, "\n"), p.Config.CommandTimeout)
|
||||||
|
if err != nil {
|
||||||
|
errChannel <- err
|
||||||
|
} else {
|
||||||
|
// read from the output channel until the done signal is passed
|
||||||
|
isTimeout := true
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case isTimeout = <-doneChan:
|
||||||
|
break loop
|
||||||
|
case outline := <-stdoutChan:
|
||||||
|
p.log(host, "out:", outline)
|
||||||
|
case errline := <-stderrChan:
|
||||||
|
p.log(host, "err:", errline)
|
||||||
|
case err = <-errChan:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get exit code or command error.
|
||||||
|
if err != nil {
|
||||||
|
errChannel <- err
|
||||||
|
}
|
||||||
|
|
||||||
|
// command time out
|
||||||
|
if !isTimeout {
|
||||||
|
errChannel <- fmt.Errorf(commandTimeOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
func (p Plugin) log(host string, message ...interface{}) {
|
func (p Plugin) log(host string, message ...interface{}) {
|
||||||
log.Printf("%s: %s", host, fmt.Sprintln(message...))
|
if p.Writer == nil {
|
||||||
|
p.Writer = os.Stdout
|
||||||
|
}
|
||||||
|
if count := len(p.Config.Host); count == 1 {
|
||||||
|
fmt.Fprintf(p.Writer, "%s", fmt.Sprintln(message...))
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(p.Writer, "%s: %s", host, fmt.Sprintln(message...))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exec executes the plugin.
|
// Exec executes the plugin.
|
||||||
func (p Plugin) Exec() error {
|
func (p Plugin) Exec() error {
|
||||||
if len(p.Config.Host) == 0 && p.Config.UserName == "" {
|
if len(p.Config.Host) == 0 && len(p.Config.UserName) == 0 {
|
||||||
return fmt.Errorf(missingHostOrUser)
|
return fmt.Errorf(missingHostOrUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.Config.Key == "" && p.Config.Password == "" && p.Config.KeyPath == "" {
|
if len(p.Config.Key) == 0 && len(p.Config.Password) == 0 && len(p.Config.KeyPath) == 0 {
|
||||||
return fmt.Errorf(missingPasswordOrKey)
|
return fmt.Errorf(missingPasswordOrKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(p.Config.Key) != 0 && len(p.Config.Password) != 0 {
|
||||||
|
return fmt.Errorf(setPasswordandKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
wg.Add(len(p.Config.Host))
|
wg.Add(len(p.Config.Host))
|
||||||
errChannel := make(chan error, 1)
|
errChannel := make(chan error, 1)
|
||||||
finished := make(chan bool, 1)
|
finished := make(chan bool, 1)
|
||||||
for _, host := range p.Config.Host {
|
for _, host := range p.Config.Host {
|
||||||
go func(host string) {
|
if p.Config.Sync {
|
||||||
// Create MakeConfig instance with remote username, server address and path to private key.
|
p.exec(host, &wg, errChannel)
|
||||||
ssh := &easyssh.MakeConfig{
|
} else {
|
||||||
Server: host,
|
go p.exec(host, &wg, errChannel)
|
||||||
User: p.Config.UserName,
|
}
|
||||||
Password: p.Config.Password,
|
|
||||||
Port: strconv.Itoa(p.Config.Port),
|
|
||||||
Key: p.Config.Key,
|
|
||||||
KeyPath: p.Config.KeyPath,
|
|
||||||
Timeout: p.Config.Timeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
p.log(host, "commands: ", strings.Join(p.Config.Script, "\n"))
|
|
||||||
response, err := ssh.Run(strings.Join(p.Config.Script, "\n"))
|
|
||||||
p.log(host, "outputs:", response)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
errChannel <- err
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Done()
|
|
||||||
}(host)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -88,12 +170,32 @@ func (p Plugin) Exec() error {
|
|||||||
case <-finished:
|
case <-finished:
|
||||||
case err := <-errChannel:
|
case err := <-errChannel:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("drone-ssh error: ", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Successfully executed commands to all host.")
|
fmt.Println("==========================================")
|
||||||
|
fmt.Println("Successfully executed commands to all host.")
|
||||||
|
fmt.Println("==========================================")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p Plugin) scriptCommands() []string {
|
||||||
|
numCommands := len(p.Config.Script)
|
||||||
|
if p.Config.ScriptStop {
|
||||||
|
numCommands *= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
commands := make([]string, numCommands)
|
||||||
|
|
||||||
|
for _, cmd := range p.Config.Script {
|
||||||
|
if p.Config.ScriptStop {
|
||||||
|
commands = append(commands, "DRONE_SSH_PREV_COMMAND_EXIT_CODE=$? ; if [ $DRONE_SSH_PREV_COMMAND_EXIT_CODE -ne 0 ]; then exit $DRONE_SSH_PREV_COMMAND_EXIT_CODE; fi;")
|
||||||
|
}
|
||||||
|
|
||||||
|
commands = append(commands, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands
|
||||||
|
}
|
||||||
|
|||||||
+441
-13
@@ -1,8 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/appleboy/easyssh-proxy"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,6 +25,7 @@ func TestMissingKeyOrPassword(t *testing.T) {
|
|||||||
Host: []string{"localhost"},
|
Host: []string{"localhost"},
|
||||||
UserName: "ubuntu",
|
UserName: "ubuntu",
|
||||||
},
|
},
|
||||||
|
os.Stdout,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := plugin.Exec()
|
err := plugin.Exec()
|
||||||
@@ -29,14 +34,32 @@ func TestMissingKeyOrPassword(t *testing.T) {
|
|||||||
assert.Equal(t, missingPasswordOrKey, err.Error())
|
assert.Equal(t, missingPasswordOrKey, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetPasswordAndKey(t *testing.T) {
|
||||||
|
plugin := Plugin{
|
||||||
|
Config{
|
||||||
|
Host: []string{"localhost"},
|
||||||
|
UserName: "ubuntu",
|
||||||
|
Password: "1234",
|
||||||
|
Key: "1234",
|
||||||
|
},
|
||||||
|
os.Stdout,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := plugin.Exec()
|
||||||
|
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Equal(t, setPasswordandKey, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
func TestIncorrectPassword(t *testing.T) {
|
func TestIncorrectPassword(t *testing.T) {
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
Config: Config{
|
Config: Config{
|
||||||
Host: []string{"localhost"},
|
Host: []string{"localhost"},
|
||||||
UserName: "drone-scp",
|
UserName: "drone-scp",
|
||||||
Port: 22,
|
Port: 22,
|
||||||
Password: "123456",
|
Password: "123456",
|
||||||
Script: []string{"whoami"},
|
Script: []string{"whoami"},
|
||||||
|
CommandTimeout: 60,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,9 +70,10 @@ func TestIncorrectPassword(t *testing.T) {
|
|||||||
func TestSSHScriptFromRawKey(t *testing.T) {
|
func TestSSHScriptFromRawKey(t *testing.T) {
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
Config: Config{
|
Config: Config{
|
||||||
Host: []string{"localhost"},
|
Host: []string{"localhost"},
|
||||||
UserName: "drone-scp",
|
UserName: "drone-scp",
|
||||||
Port: 22,
|
Port: 22,
|
||||||
|
CommandTimeout: 60,
|
||||||
Key: `-----BEGIN RSA PRIVATE KEY-----
|
Key: `-----BEGIN RSA PRIVATE KEY-----
|
||||||
MIIEpAIBAAKCAQEA4e2D/qPN08pzTac+a8ZmlP1ziJOXk45CynMPtva0rtK/RB26
|
MIIEpAIBAAKCAQEA4e2D/qPN08pzTac+a8ZmlP1ziJOXk45CynMPtva0rtK/RB26
|
||||||
VbfAF0hIJji7ltvnYnqCU9oFfvEM33cTn7T96+od8ib/Vz25YU8ZbstqtIskPuwC
|
VbfAF0hIJji7ltvnYnqCU9oFfvEM33cTn7T96+od8ib/Vz25YU8ZbstqtIskPuwC
|
||||||
@@ -89,14 +113,418 @@ ib4KbP5ovZlrjL++akMQ7V2fHzuQIFWnCkDA5c2ZAqzlM+ZN+HRG7gWur7Bt4XH1
|
|||||||
func TestSSHScriptFromKeyFile(t *testing.T) {
|
func TestSSHScriptFromKeyFile(t *testing.T) {
|
||||||
plugin := Plugin{
|
plugin := Plugin{
|
||||||
Config: Config{
|
Config: Config{
|
||||||
Host: []string{"localhost", "127.0.0.1"},
|
Host: []string{"localhost", "127.0.0.1"},
|
||||||
UserName: "drone-scp",
|
UserName: "drone-scp",
|
||||||
Port: 22,
|
Port: 22,
|
||||||
KeyPath: "./tests/.ssh/id_rsa",
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
Script: []string{"whoami", "ls -al"},
|
Script: []string{"whoami", "ls -al"},
|
||||||
|
CommandTimeout: 60,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := plugin.Exec()
|
err := plugin.Exec()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStreamFromSSHCommand(t *testing.T) {
|
||||||
|
plugin := Plugin{
|
||||||
|
Config: Config{
|
||||||
|
Host: []string{"localhost", "127.0.0.1"},
|
||||||
|
UserName: "drone-scp",
|
||||||
|
Port: 22,
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
Script: []string{"whoami", "for i in {1..5}; do echo ${i}; sleep 1; done", "echo 'done'"},
|
||||||
|
CommandTimeout: 60,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := plugin.Exec()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSHScriptWithError(t *testing.T) {
|
||||||
|
plugin := Plugin{
|
||||||
|
Config: Config{
|
||||||
|
Host: []string{"localhost", "127.0.0.1"},
|
||||||
|
UserName: "drone-scp",
|
||||||
|
Port: 22,
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
Script: []string{"exit 1"},
|
||||||
|
CommandTimeout: 60,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := plugin.Exec()
|
||||||
|
// Process exited with status 1
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSHCommandTimeOut(t *testing.T) {
|
||||||
|
plugin := Plugin{
|
||||||
|
Config: Config{
|
||||||
|
Host: []string{"localhost"},
|
||||||
|
UserName: "drone-scp",
|
||||||
|
Port: 22,
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
Script: []string{"sleep 5"},
|
||||||
|
CommandTimeout: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := plugin.Exec()
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProxyCommand(t *testing.T) {
|
||||||
|
plugin := Plugin{
|
||||||
|
Config: Config{
|
||||||
|
Host: []string{"localhost"},
|
||||||
|
UserName: "drone-scp",
|
||||||
|
Port: 22,
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
Script: []string{"whoami"},
|
||||||
|
CommandTimeout: 1,
|
||||||
|
Proxy: easyssh.DefaultConfig{
|
||||||
|
Server: "localhost",
|
||||||
|
User: "drone-scp",
|
||||||
|
Port: "22",
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := plugin.Exec()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSHCommandError(t *testing.T) {
|
||||||
|
plugin := Plugin{
|
||||||
|
Config: Config{
|
||||||
|
Host: []string{"localhost"},
|
||||||
|
UserName: "drone-scp",
|
||||||
|
Port: 22,
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
Script: []string{"mkdir a", "mkdir a"},
|
||||||
|
CommandTimeout: 60,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := plugin.Exec()
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSHCommandExitCodeError(t *testing.T) {
|
||||||
|
plugin := Plugin{
|
||||||
|
Config: Config{
|
||||||
|
Host: []string{"localhost"},
|
||||||
|
UserName: "drone-scp",
|
||||||
|
Port: 22,
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
Script: []string{
|
||||||
|
"set -e",
|
||||||
|
"echo 1",
|
||||||
|
"mkdir a",
|
||||||
|
"mkdir a",
|
||||||
|
"echo 2",
|
||||||
|
},
|
||||||
|
CommandTimeout: 60,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := plugin.Exec()
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetENV(t *testing.T) {
|
||||||
|
os.Setenv("FOO", `' 1) '`)
|
||||||
|
plugin := Plugin{
|
||||||
|
Config: Config{
|
||||||
|
Host: []string{"localhost"},
|
||||||
|
UserName: "drone-scp",
|
||||||
|
Port: 22,
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
Secrets: []string{"FOO"},
|
||||||
|
Envs: []string{"foo"},
|
||||||
|
Debug: true,
|
||||||
|
Script: []string{"whoami; echo $FOO"},
|
||||||
|
CommandTimeout: 1,
|
||||||
|
Proxy: easyssh.DefaultConfig{
|
||||||
|
Server: "localhost",
|
||||||
|
User: "drone-scp",
|
||||||
|
Port: "22",
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := plugin.Exec()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetExistingENV(t *testing.T) {
|
||||||
|
os.Setenv("FOO", "Value for foo")
|
||||||
|
os.Setenv("BAR", "")
|
||||||
|
plugin := Plugin{
|
||||||
|
Config: Config{
|
||||||
|
Host: []string{"localhost"},
|
||||||
|
UserName: "drone-scp",
|
||||||
|
Port: 22,
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
Secrets: []string{"FOO"},
|
||||||
|
Envs: []string{"foo", "bar", "baz"},
|
||||||
|
Debug: true,
|
||||||
|
Script: []string{"export FOO", "export BAR", "export BAZ", "env | grep -q '^FOO=Value for foo$'", "env | grep -q '^BAR=$'", "if env | grep -q BAZ; then false; else true; fi"},
|
||||||
|
CommandTimeout: 1,
|
||||||
|
Proxy: easyssh.DefaultConfig{
|
||||||
|
Server: "localhost",
|
||||||
|
User: "drone-scp",
|
||||||
|
Port: "22",
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := plugin.Exec()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncMode(t *testing.T) {
|
||||||
|
plugin := Plugin{
|
||||||
|
Config: Config{
|
||||||
|
Host: []string{"localhost", "127.0.0.1"},
|
||||||
|
UserName: "drone-scp",
|
||||||
|
Port: 22,
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
Script: []string{"whoami", "for i in {1..3}; do echo ${i}; sleep 1; done", "echo 'done'"},
|
||||||
|
CommandTimeout: 60,
|
||||||
|
Sync: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := plugin.Exec()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_escapeArg(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
arg string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "escape nothing",
|
||||||
|
args: args{
|
||||||
|
arg: "Hi I am appleboy",
|
||||||
|
},
|
||||||
|
want: `'Hi I am appleboy'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "escape single quote",
|
||||||
|
args: args{
|
||||||
|
arg: "Hi I am 'appleboy'",
|
||||||
|
},
|
||||||
|
want: `'Hi I am '\''appleboy'\'''`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := escapeArg(tt.args.arg)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandOutput(t *testing.T) {
|
||||||
|
var (
|
||||||
|
buffer bytes.Buffer
|
||||||
|
expected = `
|
||||||
|
localhost: ======CMD======
|
||||||
|
localhost: pwd
|
||||||
|
whoami
|
||||||
|
uname
|
||||||
|
localhost: ======END======
|
||||||
|
localhost: out: /home/drone-scp
|
||||||
|
localhost: out: drone-scp
|
||||||
|
localhost: out: Linux
|
||||||
|
127.0.0.1: ======CMD======
|
||||||
|
127.0.0.1: pwd
|
||||||
|
whoami
|
||||||
|
uname
|
||||||
|
127.0.0.1: ======END======
|
||||||
|
127.0.0.1: out: /home/drone-scp
|
||||||
|
127.0.0.1: out: drone-scp
|
||||||
|
127.0.0.1: out: Linux
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
plugin := Plugin{
|
||||||
|
Config: Config{
|
||||||
|
Host: []string{"localhost", "127.0.0.1"},
|
||||||
|
UserName: "drone-scp",
|
||||||
|
Port: 22,
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
Script: []string{
|
||||||
|
"pwd",
|
||||||
|
"whoami",
|
||||||
|
"uname",
|
||||||
|
},
|
||||||
|
CommandTimeout: 60,
|
||||||
|
Sync: true,
|
||||||
|
},
|
||||||
|
Writer: &buffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := plugin.Exec()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, unindent(expected), unindent(buffer.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScriptStop(t *testing.T) {
|
||||||
|
var (
|
||||||
|
buffer bytes.Buffer
|
||||||
|
expected = `
|
||||||
|
======CMD======
|
||||||
|
mkdir a/b/c
|
||||||
|
mkdir d/e/f
|
||||||
|
======END======
|
||||||
|
err: mkdir: can't create directory 'a/b/c': No such file or directory
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
plugin := Plugin{
|
||||||
|
Config: Config{
|
||||||
|
Host: []string{"localhost"},
|
||||||
|
UserName: "drone-scp",
|
||||||
|
Port: 22,
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
Script: []string{
|
||||||
|
"mkdir a/b/c",
|
||||||
|
"mkdir d/e/f",
|
||||||
|
},
|
||||||
|
CommandTimeout: 10,
|
||||||
|
ScriptStop: true,
|
||||||
|
},
|
||||||
|
Writer: &buffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := plugin.Exec()
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, unindent(expected), unindent(buffer.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoneScriptStop(t *testing.T) {
|
||||||
|
var (
|
||||||
|
buffer bytes.Buffer
|
||||||
|
expected = `
|
||||||
|
======CMD======
|
||||||
|
mkdir a/b/c
|
||||||
|
mkdir d/e/f
|
||||||
|
======END======
|
||||||
|
err: mkdir: can't create directory 'a/b/c': No such file or directory
|
||||||
|
err: mkdir: can't create directory 'd/e/f': No such file or directory
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
plugin := Plugin{
|
||||||
|
Config: Config{
|
||||||
|
Host: []string{"localhost"},
|
||||||
|
UserName: "drone-scp",
|
||||||
|
Port: 22,
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
Script: []string{
|
||||||
|
"mkdir a/b/c",
|
||||||
|
"mkdir d/e/f",
|
||||||
|
},
|
||||||
|
CommandTimeout: 10,
|
||||||
|
},
|
||||||
|
Writer: &buffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := plugin.Exec()
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, unindent(expected), unindent(buffer.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvOutput(t *testing.T) {
|
||||||
|
var (
|
||||||
|
buffer bytes.Buffer
|
||||||
|
expected = `
|
||||||
|
======CMD======
|
||||||
|
echo "[${ENV_1}]"
|
||||||
|
echo "[${ENV_2}]"
|
||||||
|
echo "[${ENV_3}]"
|
||||||
|
echo "[${ENV_4}]"
|
||||||
|
echo "[${ENV_5}]"
|
||||||
|
echo "[${ENV_6}]"
|
||||||
|
echo "[${ENV_7}]"
|
||||||
|
======END======
|
||||||
|
======ENV======
|
||||||
|
ENV_1='test'
|
||||||
|
ENV_2='test test'
|
||||||
|
ENV_3='test '
|
||||||
|
ENV_4=' test test '
|
||||||
|
ENV_5='test'\'''
|
||||||
|
ENV_6='test"'
|
||||||
|
ENV_7='test,!#;?.@$~'\''"'
|
||||||
|
======END======
|
||||||
|
out: [test]
|
||||||
|
out: [test test]
|
||||||
|
out: [test ]
|
||||||
|
out: [ test test ]
|
||||||
|
out: [test']
|
||||||
|
out: [test"]
|
||||||
|
out: [test,!#;?.@$~'"]
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
os.Setenv("ENV_1", `test`)
|
||||||
|
os.Setenv("ENV_2", `test test`)
|
||||||
|
os.Setenv("ENV_3", `test `)
|
||||||
|
os.Setenv("ENV_4", ` test test `)
|
||||||
|
os.Setenv("ENV_5", `test'`)
|
||||||
|
os.Setenv("ENV_6", `test"`)
|
||||||
|
os.Setenv("ENV_7", `test,!#;?.@$~'"`)
|
||||||
|
|
||||||
|
plugin := Plugin{
|
||||||
|
Config: Config{
|
||||||
|
Host: []string{"localhost"},
|
||||||
|
UserName: "drone-scp",
|
||||||
|
Port: 22,
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
Envs: []string{"env_1", "env_2", "env_3", "env_4", "env_5", "env_6", "env_7"},
|
||||||
|
Debug: true,
|
||||||
|
Script: []string{
|
||||||
|
`echo "[${ENV_1}]"`,
|
||||||
|
`echo "[${ENV_2}]"`,
|
||||||
|
`echo "[${ENV_3}]"`,
|
||||||
|
`echo "[${ENV_4}]"`,
|
||||||
|
`echo "[${ENV_5}]"`,
|
||||||
|
`echo "[${ENV_6}]"`,
|
||||||
|
`echo "[${ENV_7}]"`,
|
||||||
|
},
|
||||||
|
CommandTimeout: 10,
|
||||||
|
Proxy: easyssh.DefaultConfig{
|
||||||
|
Server: "localhost",
|
||||||
|
User: "drone-scp",
|
||||||
|
Port: "22",
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Writer: &buffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := plugin.Exec()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, unindent(expected), unindent(buffer.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func unindent(text string) string {
|
||||||
|
return strings.TrimSpace(strings.Replace(text, "\t", "", -1))
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
+21
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Bo-Yi Wu
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
.PHONY: test drone-ssh fmt vet errcheck lint install update coverage embedmd
|
||||||
|
|
||||||
|
GOFMT ?= gofmt "-s"
|
||||||
|
GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*")
|
||||||
|
PACKAGES ?= $(shell go list ./... | grep -v /vendor/)
|
||||||
|
|
||||||
|
all: install lint
|
||||||
|
|
||||||
|
install:
|
||||||
|
@hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
|
go get -u github.com/kardianos/govendor; \
|
||||||
|
fi
|
||||||
|
govendor sync
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
$(GOFMT) -w $(GOFILES)
|
||||||
|
|
||||||
|
.PHONY: fmt-check
|
||||||
|
fmt-check:
|
||||||
|
# get all go files and run go fmt on them
|
||||||
|
@diff=$$($(GOFMT) -d $(GOFILES)); \
|
||||||
|
if [ -n "$$diff" ]; then \
|
||||||
|
echo "Please run 'make fmt' and commit the result:"; \
|
||||||
|
echo "$${diff}"; \
|
||||||
|
exit 1; \
|
||||||
|
fi;
|
||||||
|
|
||||||
|
vet:
|
||||||
|
go vet $(PACKAGES)
|
||||||
|
|
||||||
|
errcheck:
|
||||||
|
@hash errcheck > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
|
go get -u github.com/kisielk/errcheck; \
|
||||||
|
fi
|
||||||
|
errcheck $(PACKAGES)
|
||||||
|
|
||||||
|
lint:
|
||||||
|
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
|
go get -u github.com/golang/lint/golint; \
|
||||||
|
fi
|
||||||
|
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
|
||||||
|
|
||||||
|
unconvert:
|
||||||
|
@hash unconvert > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
|
go get -u github.com/mdempsky/unconvert; \
|
||||||
|
fi
|
||||||
|
for PKG in $(PACKAGES); do unconvert -v $$PKG || exit 1; done;
|
||||||
|
|
||||||
|
embedmd:
|
||||||
|
@hash embedmd > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
|
go get -u github.com/campoy/embedmd; \
|
||||||
|
fi
|
||||||
|
embedmd -d *.md
|
||||||
|
|
||||||
|
test: fmt-check
|
||||||
|
for PKG in $(PACKAGES); do go test -v -cover -coverprofile $$GOPATH/src/$$PKG/coverage.txt $$PKG || exit 1; done;
|
||||||
|
|
||||||
|
html:
|
||||||
|
go tool cover -html=coverage.txt
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
sed -i '/main.go/d' .cover/coverage.txt
|
||||||
|
curl -s https://codecov.io/bash > .codecov && \
|
||||||
|
chmod +x .codecov && \
|
||||||
|
./.codecov -f .cover/coverage.txt
|
||||||
|
|
||||||
|
clean:
|
||||||
|
go clean -x -i ./...
|
||||||
|
rm -rf coverage.txt $(EXECUTABLE) $(DIST) vendor
|
||||||
|
|
||||||
|
ssh-server:
|
||||||
|
adduser -h /home/drone-scp -s /bin/bash -D -S drone-scp
|
||||||
|
echo drone-scp:1234 | chpasswd
|
||||||
|
mkdir -p /home/drone-scp/.ssh
|
||||||
|
chmod 700 /home/drone-scp/.ssh
|
||||||
|
cp tests/.ssh/id_rsa.pub /home/drone-scp/.ssh/authorized_keys
|
||||||
|
chown -R drone-scp /home/drone-scp/.ssh
|
||||||
|
# install ssh and start server
|
||||||
|
apk add --update openssh openrc
|
||||||
|
rm -rf /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_dsa_key
|
||||||
|
./tests/entrypoint.sh /usr/sbin/sshd -D &
|
||||||
|
|
||||||
|
version:
|
||||||
|
@echo $(VERSION)
|
||||||
+181
@@ -0,0 +1,181 @@
|
|||||||
|
# easyssh-proxy
|
||||||
|
|
||||||
|
[](https://godoc.org/github.com/appleboy/easyssh-proxy)
|
||||||
|
[](http://drone.wu-boy.com/appleboy/easyssh-proxy)
|
||||||
|
[](https://codecov.io/gh/appleboy/easyssh-proxy)
|
||||||
|
[](https://goreportcard.com/report/github.com/appleboy/easyssh-proxy)
|
||||||
|
[](https://sourcegraph.com/github.com/appleboy/easyssh-proxy?badge)
|
||||||
|
[](https://github.com/appleboy/easyssh-proxy/releases/latest)
|
||||||
|
|
||||||
|
easyssh-proxy provides a simple implementation of some SSH protocol features in Go.
|
||||||
|
|
||||||
|
## Feature
|
||||||
|
|
||||||
|
This project is forked from [easyssh](https://github.com/hypersleep/easyssh) but add some features as the following.
|
||||||
|
|
||||||
|
* [x] Support plain text of user private key.
|
||||||
|
* [x] Support key path of user private key.
|
||||||
|
* [x] Support Timeout for the TCP connection to establish.
|
||||||
|
* [x] Support SSH ProxyCommand.
|
||||||
|
|
||||||
|
```
|
||||||
|
+--------+ +----------+ +-----------+
|
||||||
|
| Laptop | <--> | Jumphost | <--> | FooServer |
|
||||||
|
+--------+ +----------+ +-----------+
|
||||||
|
|
||||||
|
OR
|
||||||
|
|
||||||
|
+--------+ +----------+ +-----------+
|
||||||
|
| Laptop | <--> | Firewall | <--> | FooServer |
|
||||||
|
+--------+ +----------+ +-----------+
|
||||||
|
192.168.1.5 121.1.2.3 10.10.29.68
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage:
|
||||||
|
|
||||||
|
You can see `ssh`, `scp`, `ProxyCommand` on `examples` folder.
|
||||||
|
|
||||||
|
### ssh
|
||||||
|
|
||||||
|
See [example/ssh/ssh.go](./example/ssh/ssh.go)
|
||||||
|
|
||||||
|
[embedmd]:# (example/ssh/ssh.go go)
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/appleboy/easyssh-proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create MakeConfig instance with remote username, server address and path to private key.
|
||||||
|
ssh := &easyssh.MakeConfig{
|
||||||
|
User: "appleboy",
|
||||||
|
Server: "example.com",
|
||||||
|
// Optional key or Password without either we try to contact your agent SOCKET
|
||||||
|
//Password: "password",
|
||||||
|
Key: "/.ssh/id_rsa",
|
||||||
|
Port: "22",
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Run method with command you want to run on remote server.
|
||||||
|
stdout, stderr, done, err := ssh.Run("ls -al", 60)
|
||||||
|
// Handle errors
|
||||||
|
if err != nil {
|
||||||
|
panic("Can't run remote command: " + err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Println("don is :", done, "stdout is :", stdout, "; stderr is :", stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### scp
|
||||||
|
|
||||||
|
See [example/scp/scp.go](./example/scp/scp.go)
|
||||||
|
|
||||||
|
[embedmd]:# (example/scp/scp.go go)
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/appleboy/easyssh-proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create MakeConfig instance with remote username, server address and path to private key.
|
||||||
|
ssh := &easyssh.MakeConfig{
|
||||||
|
User: "appleboy",
|
||||||
|
Server: "example.com",
|
||||||
|
Password: "123qwe",
|
||||||
|
Port: "22",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Scp method with file you want to upload to remote server.
|
||||||
|
// Please make sure the `tmp` floder exists.
|
||||||
|
err := ssh.Scp("/root/source.csv", "/tmp/target.csv")
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
|
if err != nil {
|
||||||
|
panic("Can't run remote command: " + err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Println("success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSH ProxyCommand
|
||||||
|
|
||||||
|
See [example/proxy/proxy.go](./example/proxy/proxy.go)
|
||||||
|
|
||||||
|
[embedmd]:# (example/proxy/proxy.go go /\tssh :=/ /\t}$/)
|
||||||
|
```go
|
||||||
|
ssh := &easyssh.MakeConfig{
|
||||||
|
User: "drone-scp",
|
||||||
|
Server: "localhost",
|
||||||
|
Port: "22",
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
Proxy: easyssh.DefaultConfig{
|
||||||
|
User: "drone-scp",
|
||||||
|
Server: "localhost",
|
||||||
|
Port: "22",
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSH Stream Log
|
||||||
|
|
||||||
|
See [example/stream/stream.go](./example/stream/stream.go)
|
||||||
|
|
||||||
|
[embedmd]:# (example/stream/stream.go go /func/ /^}$/)
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
// Create MakeConfig instance with remote username, server address and path to private key.
|
||||||
|
ssh := &easyssh.MakeConfig{
|
||||||
|
Server: "localhost",
|
||||||
|
User: "drone-scp",
|
||||||
|
KeyPath: "./tests/.ssh/id_rsa",
|
||||||
|
Port: "22",
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Run method with command you want to run on remote server.
|
||||||
|
stdoutChan, stderrChan, doneChan, errChan, err := ssh.Stream("for i in {1..5}; do echo ${i}; sleep 1; done; exit 2;", 60)
|
||||||
|
// Handle errors
|
||||||
|
if err != nil {
|
||||||
|
panic("Can't run remote command: " + err.Error())
|
||||||
|
} else {
|
||||||
|
// read from the output channel until the done signal is passed
|
||||||
|
isTimeout := true
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case isTimeout = <-doneChan:
|
||||||
|
break loop
|
||||||
|
case outline := <-stdoutChan:
|
||||||
|
fmt.Println("out:", outline)
|
||||||
|
case errline := <-stderrChan:
|
||||||
|
fmt.Println("err:", errline)
|
||||||
|
case err = <-errChan:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get exit code or command error.
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("err: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// command time out
|
||||||
|
if !isTimeout {
|
||||||
|
fmt.Println("Error: command timeout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
+300
@@ -0,0 +1,300 @@
|
|||||||
|
// Package easyssh provides a simple implementation of some SSH protocol
|
||||||
|
// features in Go. You can simply run a command on a remote server or get a file
|
||||||
|
// even simpler than native console SSH client. You don't need to think about
|
||||||
|
// Dials, sessions, defers, or public keys... Let easyssh think about it!
|
||||||
|
package easyssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"golang.org/x/crypto/ssh/agent"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// MakeConfig Contains main authority information.
|
||||||
|
// User field should be a name of user on remote server (ex. john in ssh john@example.com).
|
||||||
|
// Server field should be a remote machine address (ex. example.com in ssh john@example.com)
|
||||||
|
// Key is a path to private key on your local machine.
|
||||||
|
// Port is SSH server port on remote machine.
|
||||||
|
// Note: easyssh looking for private key in user's home directory (ex. /home/john + Key).
|
||||||
|
// Then ensure your Key begins from '/' (ex. /.ssh/id_rsa)
|
||||||
|
MakeConfig struct {
|
||||||
|
User string
|
||||||
|
Server string
|
||||||
|
Key string
|
||||||
|
KeyPath string
|
||||||
|
Port string
|
||||||
|
Password string
|
||||||
|
Timeout time.Duration
|
||||||
|
Proxy DefaultConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfig for ssh proxy config
|
||||||
|
DefaultConfig struct {
|
||||||
|
User string
|
||||||
|
Server string
|
||||||
|
Key string
|
||||||
|
KeyPath string
|
||||||
|
Port string
|
||||||
|
Password string
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// returns ssh.Signer from user you running app home path + cutted key path.
|
||||||
|
// (ex. pubkey,err := getKeyFile("/.ssh/id_rsa") )
|
||||||
|
func getKeyFile(keypath string) (ssh.Signer, error) {
|
||||||
|
buf, err := ioutil.ReadFile(keypath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pubkey, err := ssh.ParsePrivateKey(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pubkey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSSHConfig(config DefaultConfig) *ssh.ClientConfig {
|
||||||
|
// auths holds the detected ssh auth methods
|
||||||
|
auths := []ssh.AuthMethod{}
|
||||||
|
|
||||||
|
// figure out what auths are requested, what is supported
|
||||||
|
if config.Password != "" {
|
||||||
|
auths = append(auths, ssh.Password(config.Password))
|
||||||
|
}
|
||||||
|
|
||||||
|
if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil {
|
||||||
|
auths = append(auths, ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers))
|
||||||
|
defer sshAgent.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.KeyPath != "" {
|
||||||
|
if pubkey, err := getKeyFile(config.KeyPath); err == nil {
|
||||||
|
auths = append(auths, ssh.PublicKeys(pubkey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Key != "" {
|
||||||
|
if signer, err := ssh.ParsePrivateKey([]byte(config.Key)); err == nil {
|
||||||
|
auths = append(auths, ssh.PublicKeys(signer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ssh.ClientConfig{
|
||||||
|
Timeout: config.Timeout,
|
||||||
|
User: config.User,
|
||||||
|
Auth: auths,
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect to remote server using MakeConfig struct and returns *ssh.Session
|
||||||
|
func (ssh_conf *MakeConfig) connect() (*ssh.Session, error) {
|
||||||
|
var client *ssh.Client
|
||||||
|
var err error
|
||||||
|
|
||||||
|
targetConfig := getSSHConfig(DefaultConfig{
|
||||||
|
User: ssh_conf.User,
|
||||||
|
Key: ssh_conf.Key,
|
||||||
|
KeyPath: ssh_conf.KeyPath,
|
||||||
|
Password: ssh_conf.Password,
|
||||||
|
Timeout: ssh_conf.Timeout,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Enable proxy command
|
||||||
|
if ssh_conf.Proxy.Server != "" {
|
||||||
|
proxyConfig := getSSHConfig(DefaultConfig{
|
||||||
|
User: ssh_conf.Proxy.User,
|
||||||
|
Key: ssh_conf.Proxy.Key,
|
||||||
|
KeyPath: ssh_conf.Proxy.KeyPath,
|
||||||
|
Password: ssh_conf.Proxy.Password,
|
||||||
|
Timeout: ssh_conf.Proxy.Timeout,
|
||||||
|
})
|
||||||
|
|
||||||
|
proxyClient, err := ssh.Dial("tcp", net.JoinHostPort(ssh_conf.Proxy.Server, ssh_conf.Proxy.Port), proxyConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := proxyClient.Dial("tcp", net.JoinHostPort(ssh_conf.Server, ssh_conf.Port))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ncc, chans, reqs, err := ssh.NewClientConn(conn, net.JoinHostPort(ssh_conf.Server, ssh_conf.Port), targetConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client = ssh.NewClient(ncc, chans, reqs)
|
||||||
|
} else {
|
||||||
|
client, err = ssh.Dial("tcp", net.JoinHostPort(ssh_conf.Server, ssh_conf.Port), targetConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := client.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream returns one channel that combines the stdout and stderr of the command
|
||||||
|
// as it is run on the remote machine, and another that sends true when the
|
||||||
|
// command is done. The sessions and channels will then be closed.
|
||||||
|
func (ssh_conf *MakeConfig) Stream(command string, timeout int) (<-chan string, <-chan string, <-chan bool, <-chan error, error) {
|
||||||
|
// continuously send the command's output over the channel
|
||||||
|
stdoutChan := make(chan string)
|
||||||
|
stderrChan := make(chan string)
|
||||||
|
doneChan := make(chan bool)
|
||||||
|
errChan := make(chan error)
|
||||||
|
|
||||||
|
// connect to remote host
|
||||||
|
session, err := ssh_conf.connect()
|
||||||
|
if err != nil {
|
||||||
|
return stdoutChan, stderrChan, doneChan, errChan, err
|
||||||
|
}
|
||||||
|
// defer session.Close()
|
||||||
|
// connect to both outputs (they are of type io.Reader)
|
||||||
|
outReader, err := session.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return stdoutChan, stderrChan, doneChan, errChan, err
|
||||||
|
}
|
||||||
|
errReader, err := session.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return stdoutChan, stderrChan, doneChan, errChan, err
|
||||||
|
}
|
||||||
|
err = session.Start(command)
|
||||||
|
if err != nil {
|
||||||
|
return stdoutChan, stderrChan, doneChan, errChan, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// combine outputs, create a line-by-line scanner
|
||||||
|
stdoutReader := io.MultiReader(outReader)
|
||||||
|
stderrReader := io.MultiReader(errReader)
|
||||||
|
stdoutScanner := bufio.NewScanner(stdoutReader)
|
||||||
|
stderrScanner := bufio.NewScanner(stderrReader)
|
||||||
|
|
||||||
|
go func(stdoutScanner, stderrScanner *bufio.Scanner, stdoutChan, stderrChan chan string, doneChan chan bool, errChan chan error) {
|
||||||
|
defer close(stdoutChan)
|
||||||
|
defer close(stderrChan)
|
||||||
|
defer close(doneChan)
|
||||||
|
defer close(errChan)
|
||||||
|
defer session.Close()
|
||||||
|
|
||||||
|
timeoutChan := time.After(time.Duration(timeout) * time.Second)
|
||||||
|
res := make(chan bool, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for stdoutScanner.Scan() {
|
||||||
|
stdoutChan <- stdoutScanner.Text()
|
||||||
|
}
|
||||||
|
for stderrScanner.Scan() {
|
||||||
|
stderrChan <- stderrScanner.Text()
|
||||||
|
}
|
||||||
|
// close all of our open resources
|
||||||
|
res <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-res:
|
||||||
|
errChan <- session.Wait()
|
||||||
|
doneChan <- true
|
||||||
|
case <-timeoutChan:
|
||||||
|
stderrChan <- "Run Command Timeout!"
|
||||||
|
errChan <- nil
|
||||||
|
doneChan <- false
|
||||||
|
}
|
||||||
|
}(stdoutScanner, stderrScanner, stdoutChan, stderrChan, doneChan, errChan)
|
||||||
|
|
||||||
|
return stdoutChan, stderrChan, doneChan, errChan, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run command on remote machine and returns its stdout as a string
|
||||||
|
func (ssh_conf *MakeConfig) Run(command string, timeout int) (outStr string, errStr string, isTimeout bool, err error) {
|
||||||
|
stdoutChan, stderrChan, doneChan, errChan, err := ssh_conf.Stream(command, timeout)
|
||||||
|
if err != nil {
|
||||||
|
return outStr, errStr, isTimeout, err
|
||||||
|
}
|
||||||
|
// read from the output channel until the done signal is passed
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case isTimeout = <-doneChan:
|
||||||
|
break loop
|
||||||
|
case outline := <-stdoutChan:
|
||||||
|
if outline != "" {
|
||||||
|
outStr += outline + "\n"
|
||||||
|
}
|
||||||
|
case errline := <-stderrChan:
|
||||||
|
if errline != "" {
|
||||||
|
errStr += errline + "\n"
|
||||||
|
}
|
||||||
|
case err = <-errChan:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return the concatenation of all signals from the output channel
|
||||||
|
return outStr, errStr, isTimeout, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scp uploads sourceFile to remote machine like native scp console app.
|
||||||
|
func (ssh_conf *MakeConfig) Scp(sourceFile string, etargetFile string) error {
|
||||||
|
session, err := ssh_conf.connect()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer session.Close()
|
||||||
|
|
||||||
|
targetFile := filepath.Base(etargetFile)
|
||||||
|
|
||||||
|
src, srcErr := os.Open(sourceFile)
|
||||||
|
|
||||||
|
if srcErr != nil {
|
||||||
|
return srcErr
|
||||||
|
}
|
||||||
|
|
||||||
|
srcStat, statErr := src.Stat()
|
||||||
|
|
||||||
|
if statErr != nil {
|
||||||
|
return statErr
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
w, err := session.StdinPipe()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer w.Close()
|
||||||
|
|
||||||
|
fmt.Fprintln(w, "C0644", srcStat.Size(), targetFile)
|
||||||
|
|
||||||
|
if srcStat.Size() > 0 {
|
||||||
|
io.Copy(w, src)
|
||||||
|
fmt.Fprint(w, "\x00")
|
||||||
|
} else {
|
||||||
|
fmt.Fprint(w, "\x00")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := session.Run(fmt.Sprintf("scp -tr %s", etargetFile)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+659
@@ -0,0 +1,659 @@
|
|||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package agent implements the ssh-agent protocol, and provides both
|
||||||
|
// a client and a server. The client can talk to a standard ssh-agent
|
||||||
|
// that uses UNIX sockets, and one could implement an alternative
|
||||||
|
// ssh-agent process using the sample server.
|
||||||
|
//
|
||||||
|
// References:
|
||||||
|
// [PROTOCOL.agent]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.agent?rev=HEAD
|
||||||
|
package agent // import "golang.org/x/crypto/ssh/agent"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/dsa"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rsa"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Agent represents the capabilities of an ssh-agent.
|
||||||
|
type Agent interface {
|
||||||
|
// List returns the identities known to the agent.
|
||||||
|
List() ([]*Key, error)
|
||||||
|
|
||||||
|
// Sign has the agent sign the data using a protocol 2 key as defined
|
||||||
|
// in [PROTOCOL.agent] section 2.6.2.
|
||||||
|
Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error)
|
||||||
|
|
||||||
|
// Add adds a private key to the agent.
|
||||||
|
Add(key AddedKey) error
|
||||||
|
|
||||||
|
// Remove removes all identities with the given public key.
|
||||||
|
Remove(key ssh.PublicKey) error
|
||||||
|
|
||||||
|
// RemoveAll removes all identities.
|
||||||
|
RemoveAll() error
|
||||||
|
|
||||||
|
// Lock locks the agent. Sign and Remove will fail, and List will empty an empty list.
|
||||||
|
Lock(passphrase []byte) error
|
||||||
|
|
||||||
|
// Unlock undoes the effect of Lock
|
||||||
|
Unlock(passphrase []byte) error
|
||||||
|
|
||||||
|
// Signers returns signers for all the known keys.
|
||||||
|
Signers() ([]ssh.Signer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddedKey describes an SSH key to be added to an Agent.
|
||||||
|
type AddedKey struct {
|
||||||
|
// PrivateKey must be a *rsa.PrivateKey, *dsa.PrivateKey or
|
||||||
|
// *ecdsa.PrivateKey, which will be inserted into the agent.
|
||||||
|
PrivateKey interface{}
|
||||||
|
// Certificate, if not nil, is communicated to the agent and will be
|
||||||
|
// stored with the key.
|
||||||
|
Certificate *ssh.Certificate
|
||||||
|
// Comment is an optional, free-form string.
|
||||||
|
Comment string
|
||||||
|
// LifetimeSecs, if not zero, is the number of seconds that the
|
||||||
|
// agent will store the key for.
|
||||||
|
LifetimeSecs uint32
|
||||||
|
// ConfirmBeforeUse, if true, requests that the agent confirm with the
|
||||||
|
// user before each use of this key.
|
||||||
|
ConfirmBeforeUse bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// See [PROTOCOL.agent], section 3.
|
||||||
|
const (
|
||||||
|
agentRequestV1Identities = 1
|
||||||
|
agentRemoveAllV1Identities = 9
|
||||||
|
|
||||||
|
// 3.2 Requests from client to agent for protocol 2 key operations
|
||||||
|
agentAddIdentity = 17
|
||||||
|
agentRemoveIdentity = 18
|
||||||
|
agentRemoveAllIdentities = 19
|
||||||
|
agentAddIdConstrained = 25
|
||||||
|
|
||||||
|
// 3.3 Key-type independent requests from client to agent
|
||||||
|
agentAddSmartcardKey = 20
|
||||||
|
agentRemoveSmartcardKey = 21
|
||||||
|
agentLock = 22
|
||||||
|
agentUnlock = 23
|
||||||
|
agentAddSmartcardKeyConstrained = 26
|
||||||
|
|
||||||
|
// 3.7 Key constraint identifiers
|
||||||
|
agentConstrainLifetime = 1
|
||||||
|
agentConstrainConfirm = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// maxAgentResponseBytes is the maximum agent reply size that is accepted. This
|
||||||
|
// is a sanity check, not a limit in the spec.
|
||||||
|
const maxAgentResponseBytes = 16 << 20
|
||||||
|
|
||||||
|
// Agent messages:
|
||||||
|
// These structures mirror the wire format of the corresponding ssh agent
|
||||||
|
// messages found in [PROTOCOL.agent].
|
||||||
|
|
||||||
|
// 3.4 Generic replies from agent to client
|
||||||
|
const agentFailure = 5
|
||||||
|
|
||||||
|
type failureAgentMsg struct{}
|
||||||
|
|
||||||
|
const agentSuccess = 6
|
||||||
|
|
||||||
|
type successAgentMsg struct{}
|
||||||
|
|
||||||
|
// See [PROTOCOL.agent], section 2.5.2.
|
||||||
|
const agentRequestIdentities = 11
|
||||||
|
|
||||||
|
type requestIdentitiesAgentMsg struct{}
|
||||||
|
|
||||||
|
// See [PROTOCOL.agent], section 2.5.2.
|
||||||
|
const agentIdentitiesAnswer = 12
|
||||||
|
|
||||||
|
type identitiesAnswerAgentMsg struct {
|
||||||
|
NumKeys uint32 `sshtype:"12"`
|
||||||
|
Keys []byte `ssh:"rest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// See [PROTOCOL.agent], section 2.6.2.
|
||||||
|
const agentSignRequest = 13
|
||||||
|
|
||||||
|
type signRequestAgentMsg struct {
|
||||||
|
KeyBlob []byte `sshtype:"13"`
|
||||||
|
Data []byte
|
||||||
|
Flags uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// See [PROTOCOL.agent], section 2.6.2.
|
||||||
|
|
||||||
|
// 3.6 Replies from agent to client for protocol 2 key operations
|
||||||
|
const agentSignResponse = 14
|
||||||
|
|
||||||
|
type signResponseAgentMsg struct {
|
||||||
|
SigBlob []byte `sshtype:"14"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type publicKey struct {
|
||||||
|
Format string
|
||||||
|
Rest []byte `ssh:"rest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key represents a protocol 2 public key as defined in
|
||||||
|
// [PROTOCOL.agent], section 2.5.2.
|
||||||
|
type Key struct {
|
||||||
|
Format string
|
||||||
|
Blob []byte
|
||||||
|
Comment string
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientErr(err error) error {
|
||||||
|
return fmt.Errorf("agent: client error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the storage form of an agent key with the format, base64
|
||||||
|
// encoded serialized key, and the comment if it is not empty.
|
||||||
|
func (k *Key) String() string {
|
||||||
|
s := string(k.Format) + " " + base64.StdEncoding.EncodeToString(k.Blob)
|
||||||
|
|
||||||
|
if k.Comment != "" {
|
||||||
|
s += " " + k.Comment
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the public key type.
|
||||||
|
func (k *Key) Type() string {
|
||||||
|
return k.Format
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal returns key blob to satisfy the ssh.PublicKey interface.
|
||||||
|
func (k *Key) Marshal() []byte {
|
||||||
|
return k.Blob
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify satisfies the ssh.PublicKey interface.
|
||||||
|
func (k *Key) Verify(data []byte, sig *ssh.Signature) error {
|
||||||
|
pubKey, err := ssh.ParsePublicKey(k.Blob)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("agent: bad public key: %v", err)
|
||||||
|
}
|
||||||
|
return pubKey.Verify(data, sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
type wireKey struct {
|
||||||
|
Format string
|
||||||
|
Rest []byte `ssh:"rest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseKey(in []byte) (out *Key, rest []byte, err error) {
|
||||||
|
var record struct {
|
||||||
|
Blob []byte
|
||||||
|
Comment string
|
||||||
|
Rest []byte `ssh:"rest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ssh.Unmarshal(in, &record); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var wk wireKey
|
||||||
|
if err := ssh.Unmarshal(record.Blob, &wk); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Key{
|
||||||
|
Format: wk.Format,
|
||||||
|
Blob: record.Blob,
|
||||||
|
Comment: record.Comment,
|
||||||
|
}, record.Rest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// client is a client for an ssh-agent process.
|
||||||
|
type client struct {
|
||||||
|
// conn is typically a *net.UnixConn
|
||||||
|
conn io.ReadWriter
|
||||||
|
// mu is used to prevent concurrent access to the agent
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient returns an Agent that talks to an ssh-agent process over
|
||||||
|
// the given connection.
|
||||||
|
func NewClient(rw io.ReadWriter) Agent {
|
||||||
|
return &client{conn: rw}
|
||||||
|
}
|
||||||
|
|
||||||
|
// call sends an RPC to the agent. On success, the reply is
|
||||||
|
// unmarshaled into reply and replyType is set to the first byte of
|
||||||
|
// the reply, which contains the type of the message.
|
||||||
|
func (c *client) call(req []byte) (reply interface{}, err error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
msg := make([]byte, 4+len(req))
|
||||||
|
binary.BigEndian.PutUint32(msg, uint32(len(req)))
|
||||||
|
copy(msg[4:], req)
|
||||||
|
if _, err = c.conn.Write(msg); err != nil {
|
||||||
|
return nil, clientErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var respSizeBuf [4]byte
|
||||||
|
if _, err = io.ReadFull(c.conn, respSizeBuf[:]); err != nil {
|
||||||
|
return nil, clientErr(err)
|
||||||
|
}
|
||||||
|
respSize := binary.BigEndian.Uint32(respSizeBuf[:])
|
||||||
|
if respSize > maxAgentResponseBytes {
|
||||||
|
return nil, clientErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, respSize)
|
||||||
|
if _, err = io.ReadFull(c.conn, buf); err != nil {
|
||||||
|
return nil, clientErr(err)
|
||||||
|
}
|
||||||
|
reply, err = unmarshal(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clientErr(err)
|
||||||
|
}
|
||||||
|
return reply, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) simpleCall(req []byte) error {
|
||||||
|
resp, err := c.call(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, ok := resp.(*successAgentMsg); ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("agent: failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) RemoveAll() error {
|
||||||
|
return c.simpleCall([]byte{agentRemoveAllIdentities})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) Remove(key ssh.PublicKey) error {
|
||||||
|
req := ssh.Marshal(&agentRemoveIdentityMsg{
|
||||||
|
KeyBlob: key.Marshal(),
|
||||||
|
})
|
||||||
|
return c.simpleCall(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) Lock(passphrase []byte) error {
|
||||||
|
req := ssh.Marshal(&agentLockMsg{
|
||||||
|
Passphrase: passphrase,
|
||||||
|
})
|
||||||
|
return c.simpleCall(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) Unlock(passphrase []byte) error {
|
||||||
|
req := ssh.Marshal(&agentUnlockMsg{
|
||||||
|
Passphrase: passphrase,
|
||||||
|
})
|
||||||
|
return c.simpleCall(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns the identities known to the agent.
|
||||||
|
func (c *client) List() ([]*Key, error) {
|
||||||
|
// see [PROTOCOL.agent] section 2.5.2.
|
||||||
|
req := []byte{agentRequestIdentities}
|
||||||
|
|
||||||
|
msg, err := c.call(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case *identitiesAnswerAgentMsg:
|
||||||
|
if msg.NumKeys > maxAgentResponseBytes/8 {
|
||||||
|
return nil, errors.New("agent: too many keys in agent reply")
|
||||||
|
}
|
||||||
|
keys := make([]*Key, msg.NumKeys)
|
||||||
|
data := msg.Keys
|
||||||
|
for i := uint32(0); i < msg.NumKeys; i++ {
|
||||||
|
var key *Key
|
||||||
|
var err error
|
||||||
|
if key, data, err = parseKey(data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keys[i] = key
|
||||||
|
}
|
||||||
|
return keys, nil
|
||||||
|
case *failureAgentMsg:
|
||||||
|
return nil, errors.New("agent: failed to list keys")
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign has the agent sign the data using a protocol 2 key as defined
|
||||||
|
// in [PROTOCOL.agent] section 2.6.2.
|
||||||
|
func (c *client) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) {
|
||||||
|
req := ssh.Marshal(signRequestAgentMsg{
|
||||||
|
KeyBlob: key.Marshal(),
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
|
||||||
|
msg, err := c.call(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case *signResponseAgentMsg:
|
||||||
|
var sig ssh.Signature
|
||||||
|
if err := ssh.Unmarshal(msg.SigBlob, &sig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &sig, nil
|
||||||
|
case *failureAgentMsg:
|
||||||
|
return nil, errors.New("agent: failed to sign challenge")
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshal parses an agent message in packet, returning the parsed
|
||||||
|
// form and the message type of packet.
|
||||||
|
func unmarshal(packet []byte) (interface{}, error) {
|
||||||
|
if len(packet) < 1 {
|
||||||
|
return nil, errors.New("agent: empty packet")
|
||||||
|
}
|
||||||
|
var msg interface{}
|
||||||
|
switch packet[0] {
|
||||||
|
case agentFailure:
|
||||||
|
return new(failureAgentMsg), nil
|
||||||
|
case agentSuccess:
|
||||||
|
return new(successAgentMsg), nil
|
||||||
|
case agentIdentitiesAnswer:
|
||||||
|
msg = new(identitiesAnswerAgentMsg)
|
||||||
|
case agentSignResponse:
|
||||||
|
msg = new(signResponseAgentMsg)
|
||||||
|
case agentV1IdentitiesAnswer:
|
||||||
|
msg = new(agentV1IdentityMsg)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("agent: unknown type tag %d", packet[0])
|
||||||
|
}
|
||||||
|
if err := ssh.Unmarshal(packet, msg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type rsaKeyMsg struct {
|
||||||
|
Type string `sshtype:"17|25"`
|
||||||
|
N *big.Int
|
||||||
|
E *big.Int
|
||||||
|
D *big.Int
|
||||||
|
Iqmp *big.Int // IQMP = Inverse Q Mod P
|
||||||
|
P *big.Int
|
||||||
|
Q *big.Int
|
||||||
|
Comments string
|
||||||
|
Constraints []byte `ssh:"rest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dsaKeyMsg struct {
|
||||||
|
Type string `sshtype:"17|25"`
|
||||||
|
P *big.Int
|
||||||
|
Q *big.Int
|
||||||
|
G *big.Int
|
||||||
|
Y *big.Int
|
||||||
|
X *big.Int
|
||||||
|
Comments string
|
||||||
|
Constraints []byte `ssh:"rest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ecdsaKeyMsg struct {
|
||||||
|
Type string `sshtype:"17|25"`
|
||||||
|
Curve string
|
||||||
|
KeyBytes []byte
|
||||||
|
D *big.Int
|
||||||
|
Comments string
|
||||||
|
Constraints []byte `ssh:"rest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ed25519KeyMsg struct {
|
||||||
|
Type string `sshtype:"17|25"`
|
||||||
|
Pub []byte
|
||||||
|
Priv []byte
|
||||||
|
Comments string
|
||||||
|
Constraints []byte `ssh:"rest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert adds a private key to the agent.
|
||||||
|
func (c *client) insertKey(s interface{}, comment string, constraints []byte) error {
|
||||||
|
var req []byte
|
||||||
|
switch k := s.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
if len(k.Primes) != 2 {
|
||||||
|
return fmt.Errorf("agent: unsupported RSA key with %d primes", len(k.Primes))
|
||||||
|
}
|
||||||
|
k.Precompute()
|
||||||
|
req = ssh.Marshal(rsaKeyMsg{
|
||||||
|
Type: ssh.KeyAlgoRSA,
|
||||||
|
N: k.N,
|
||||||
|
E: big.NewInt(int64(k.E)),
|
||||||
|
D: k.D,
|
||||||
|
Iqmp: k.Precomputed.Qinv,
|
||||||
|
P: k.Primes[0],
|
||||||
|
Q: k.Primes[1],
|
||||||
|
Comments: comment,
|
||||||
|
Constraints: constraints,
|
||||||
|
})
|
||||||
|
case *dsa.PrivateKey:
|
||||||
|
req = ssh.Marshal(dsaKeyMsg{
|
||||||
|
Type: ssh.KeyAlgoDSA,
|
||||||
|
P: k.P,
|
||||||
|
Q: k.Q,
|
||||||
|
G: k.G,
|
||||||
|
Y: k.Y,
|
||||||
|
X: k.X,
|
||||||
|
Comments: comment,
|
||||||
|
Constraints: constraints,
|
||||||
|
})
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
nistID := fmt.Sprintf("nistp%d", k.Params().BitSize)
|
||||||
|
req = ssh.Marshal(ecdsaKeyMsg{
|
||||||
|
Type: "ecdsa-sha2-" + nistID,
|
||||||
|
Curve: nistID,
|
||||||
|
KeyBytes: elliptic.Marshal(k.Curve, k.X, k.Y),
|
||||||
|
D: k.D,
|
||||||
|
Comments: comment,
|
||||||
|
Constraints: constraints,
|
||||||
|
})
|
||||||
|
case *ed25519.PrivateKey:
|
||||||
|
req = ssh.Marshal(ed25519KeyMsg{
|
||||||
|
Type: ssh.KeyAlgoED25519,
|
||||||
|
Pub: []byte(*k)[32:],
|
||||||
|
Priv: []byte(*k),
|
||||||
|
Comments: comment,
|
||||||
|
Constraints: constraints,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("agent: unsupported key type %T", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if constraints are present then the message type needs to be changed.
|
||||||
|
if len(constraints) != 0 {
|
||||||
|
req[0] = agentAddIdConstrained
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.call(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, ok := resp.(*successAgentMsg); ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("agent: failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
type rsaCertMsg struct {
|
||||||
|
Type string `sshtype:"17|25"`
|
||||||
|
CertBytes []byte
|
||||||
|
D *big.Int
|
||||||
|
Iqmp *big.Int // IQMP = Inverse Q Mod P
|
||||||
|
P *big.Int
|
||||||
|
Q *big.Int
|
||||||
|
Comments string
|
||||||
|
Constraints []byte `ssh:"rest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dsaCertMsg struct {
|
||||||
|
Type string `sshtype:"17|25"`
|
||||||
|
CertBytes []byte
|
||||||
|
X *big.Int
|
||||||
|
Comments string
|
||||||
|
Constraints []byte `ssh:"rest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ecdsaCertMsg struct {
|
||||||
|
Type string `sshtype:"17|25"`
|
||||||
|
CertBytes []byte
|
||||||
|
D *big.Int
|
||||||
|
Comments string
|
||||||
|
Constraints []byte `ssh:"rest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ed25519CertMsg struct {
|
||||||
|
Type string `sshtype:"17|25"`
|
||||||
|
CertBytes []byte
|
||||||
|
Pub []byte
|
||||||
|
Priv []byte
|
||||||
|
Comments string
|
||||||
|
Constraints []byte `ssh:"rest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds a private key to the agent. If a certificate is given,
|
||||||
|
// that certificate is added instead as public key.
|
||||||
|
func (c *client) Add(key AddedKey) error {
|
||||||
|
var constraints []byte
|
||||||
|
|
||||||
|
if secs := key.LifetimeSecs; secs != 0 {
|
||||||
|
constraints = append(constraints, agentConstrainLifetime)
|
||||||
|
|
||||||
|
var secsBytes [4]byte
|
||||||
|
binary.BigEndian.PutUint32(secsBytes[:], secs)
|
||||||
|
constraints = append(constraints, secsBytes[:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if key.ConfirmBeforeUse {
|
||||||
|
constraints = append(constraints, agentConstrainConfirm)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cert := key.Certificate; cert == nil {
|
||||||
|
return c.insertKey(key.PrivateKey, key.Comment, constraints)
|
||||||
|
} else {
|
||||||
|
return c.insertCert(key.PrivateKey, cert, key.Comment, constraints)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string, constraints []byte) error {
|
||||||
|
var req []byte
|
||||||
|
switch k := s.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
if len(k.Primes) != 2 {
|
||||||
|
return fmt.Errorf("agent: unsupported RSA key with %d primes", len(k.Primes))
|
||||||
|
}
|
||||||
|
k.Precompute()
|
||||||
|
req = ssh.Marshal(rsaCertMsg{
|
||||||
|
Type: cert.Type(),
|
||||||
|
CertBytes: cert.Marshal(),
|
||||||
|
D: k.D,
|
||||||
|
Iqmp: k.Precomputed.Qinv,
|
||||||
|
P: k.Primes[0],
|
||||||
|
Q: k.Primes[1],
|
||||||
|
Comments: comment,
|
||||||
|
Constraints: constraints,
|
||||||
|
})
|
||||||
|
case *dsa.PrivateKey:
|
||||||
|
req = ssh.Marshal(dsaCertMsg{
|
||||||
|
Type: cert.Type(),
|
||||||
|
CertBytes: cert.Marshal(),
|
||||||
|
X: k.X,
|
||||||
|
Comments: comment,
|
||||||
|
Constraints: constraints,
|
||||||
|
})
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
req = ssh.Marshal(ecdsaCertMsg{
|
||||||
|
Type: cert.Type(),
|
||||||
|
CertBytes: cert.Marshal(),
|
||||||
|
D: k.D,
|
||||||
|
Comments: comment,
|
||||||
|
Constraints: constraints,
|
||||||
|
})
|
||||||
|
case *ed25519.PrivateKey:
|
||||||
|
req = ssh.Marshal(ed25519CertMsg{
|
||||||
|
Type: cert.Type(),
|
||||||
|
CertBytes: cert.Marshal(),
|
||||||
|
Pub: []byte(*k)[32:],
|
||||||
|
Priv: []byte(*k),
|
||||||
|
Comments: comment,
|
||||||
|
Constraints: constraints,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("agent: unsupported key type %T", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if constraints are present then the message type needs to be changed.
|
||||||
|
if len(constraints) != 0 {
|
||||||
|
req[0] = agentAddIdConstrained
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := ssh.NewSignerFromKey(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if bytes.Compare(cert.Key.Marshal(), signer.PublicKey().Marshal()) != 0 {
|
||||||
|
return errors.New("agent: signer and cert have different public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.call(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, ok := resp.(*successAgentMsg); ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("agent: failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signers provides a callback for client authentication.
|
||||||
|
func (c *client) Signers() ([]ssh.Signer, error) {
|
||||||
|
keys, err := c.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []ssh.Signer
|
||||||
|
for _, k := range keys {
|
||||||
|
result = append(result, &agentKeyringSigner{c, k})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type agentKeyringSigner struct {
|
||||||
|
agent *client
|
||||||
|
pub ssh.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *agentKeyringSigner) PublicKey() ssh.PublicKey {
|
||||||
|
return s.pub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *agentKeyringSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) {
|
||||||
|
// The agent has its own entropy source, so the rand argument is ignored.
|
||||||
|
return s.agent.Sign(s.pub, data)
|
||||||
|
}
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequestAgentForwarding sets up agent forwarding for the session.
|
||||||
|
// ForwardToAgent or ForwardToRemote should be called to route
|
||||||
|
// the authentication requests.
|
||||||
|
func RequestAgentForwarding(session *ssh.Session) error {
|
||||||
|
ok, err := session.SendRequest("auth-agent-req@openssh.com", true, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return errors.New("forwarding request denied")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForwardToAgent routes authentication requests to the given keyring.
|
||||||
|
func ForwardToAgent(client *ssh.Client, keyring Agent) error {
|
||||||
|
channels := client.HandleChannelOpen(channelType)
|
||||||
|
if channels == nil {
|
||||||
|
return errors.New("agent: already have handler for " + channelType)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for ch := range channels {
|
||||||
|
channel, reqs, err := ch.Accept()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go ssh.DiscardRequests(reqs)
|
||||||
|
go func() {
|
||||||
|
ServeAgent(keyring, channel)
|
||||||
|
channel.Close()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelType = "auth-agent@openssh.com"
|
||||||
|
|
||||||
|
// ForwardToRemote routes authentication requests to the ssh-agent
|
||||||
|
// process serving on the given unix socket.
|
||||||
|
func ForwardToRemote(client *ssh.Client, addr string) error {
|
||||||
|
channels := client.HandleChannelOpen(channelType)
|
||||||
|
if channels == nil {
|
||||||
|
return errors.New("agent: already have handler for " + channelType)
|
||||||
|
}
|
||||||
|
conn, err := net.Dial("unix", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for ch := range channels {
|
||||||
|
channel, reqs, err := ch.Accept()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go ssh.DiscardRequests(reqs)
|
||||||
|
go forwardUnixSocket(channel, addr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func forwardUnixSocket(channel ssh.Channel, addr string) {
|
||||||
|
conn, err := net.Dial("unix", addr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
go func() {
|
||||||
|
io.Copy(conn, channel)
|
||||||
|
conn.(*net.UnixConn).CloseWrite()
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
io.Copy(channel, conn)
|
||||||
|
channel.CloseWrite()
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
conn.Close()
|
||||||
|
channel.Close()
|
||||||
|
}
|
||||||
+215
@@ -0,0 +1,215 @@
|
|||||||
|
// Copyright 2014 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/subtle"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type privKey struct {
|
||||||
|
signer ssh.Signer
|
||||||
|
comment string
|
||||||
|
expire *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyring struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
keys []privKey
|
||||||
|
|
||||||
|
locked bool
|
||||||
|
passphrase []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var errLocked = errors.New("agent: locked")
|
||||||
|
|
||||||
|
// NewKeyring returns an Agent that holds keys in memory. It is safe
|
||||||
|
// for concurrent use by multiple goroutines.
|
||||||
|
func NewKeyring() Agent {
|
||||||
|
return &keyring{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAll removes all identities.
|
||||||
|
func (r *keyring) RemoveAll() error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if r.locked {
|
||||||
|
return errLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
r.keys = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeLocked does the actual key removal. The caller must already be holding the
|
||||||
|
// keyring mutex.
|
||||||
|
func (r *keyring) removeLocked(want []byte) error {
|
||||||
|
found := false
|
||||||
|
for i := 0; i < len(r.keys); {
|
||||||
|
if bytes.Equal(r.keys[i].signer.PublicKey().Marshal(), want) {
|
||||||
|
found = true
|
||||||
|
r.keys[i] = r.keys[len(r.keys)-1]
|
||||||
|
r.keys = r.keys[:len(r.keys)-1]
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return errors.New("agent: key not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes all identities with the given public key.
|
||||||
|
func (r *keyring) Remove(key ssh.PublicKey) error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if r.locked {
|
||||||
|
return errLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.removeLocked(key.Marshal())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock locks the agent. Sign and Remove will fail, and List will return an empty list.
|
||||||
|
func (r *keyring) Lock(passphrase []byte) error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if r.locked {
|
||||||
|
return errLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
r.locked = true
|
||||||
|
r.passphrase = passphrase
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock undoes the effect of Lock
|
||||||
|
func (r *keyring) Unlock(passphrase []byte) error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if !r.locked {
|
||||||
|
return errors.New("agent: not locked")
|
||||||
|
}
|
||||||
|
if len(passphrase) != len(r.passphrase) || 1 != subtle.ConstantTimeCompare(passphrase, r.passphrase) {
|
||||||
|
return fmt.Errorf("agent: incorrect passphrase")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.locked = false
|
||||||
|
r.passphrase = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// expireKeysLocked removes expired keys from the keyring. If a key was added
|
||||||
|
// with a lifetimesecs contraint and seconds >= lifetimesecs seconds have
|
||||||
|
// ellapsed, it is removed. The caller *must* be holding the keyring mutex.
|
||||||
|
func (r *keyring) expireKeysLocked() {
|
||||||
|
for _, k := range r.keys {
|
||||||
|
if k.expire != nil && time.Now().After(*k.expire) {
|
||||||
|
r.removeLocked(k.signer.PublicKey().Marshal())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns the identities known to the agent.
|
||||||
|
func (r *keyring) List() ([]*Key, error) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if r.locked {
|
||||||
|
// section 2.7: locked agents return empty.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.expireKeysLocked()
|
||||||
|
var ids []*Key
|
||||||
|
for _, k := range r.keys {
|
||||||
|
pub := k.signer.PublicKey()
|
||||||
|
ids = append(ids, &Key{
|
||||||
|
Format: pub.Type(),
|
||||||
|
Blob: pub.Marshal(),
|
||||||
|
Comment: k.comment})
|
||||||
|
}
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert adds a private key to the keyring. If a certificate
|
||||||
|
// is given, that certificate is added as public key. Note that
|
||||||
|
// any constraints given are ignored.
|
||||||
|
func (r *keyring) Add(key AddedKey) error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if r.locked {
|
||||||
|
return errLocked
|
||||||
|
}
|
||||||
|
signer, err := ssh.NewSignerFromKey(key.PrivateKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cert := key.Certificate; cert != nil {
|
||||||
|
signer, err = ssh.NewCertSigner(cert, signer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p := privKey{
|
||||||
|
signer: signer,
|
||||||
|
comment: key.Comment,
|
||||||
|
}
|
||||||
|
|
||||||
|
if key.LifetimeSecs > 0 {
|
||||||
|
t := time.Now().Add(time.Duration(key.LifetimeSecs) * time.Second)
|
||||||
|
p.expire = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
r.keys = append(r.keys, p)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign returns a signature for the data.
|
||||||
|
func (r *keyring) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if r.locked {
|
||||||
|
return nil, errLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
r.expireKeysLocked()
|
||||||
|
wanted := key.Marshal()
|
||||||
|
for _, k := range r.keys {
|
||||||
|
if bytes.Equal(k.signer.PublicKey().Marshal(), wanted) {
|
||||||
|
return k.signer.Sign(rand.Reader, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signers returns signers for all the known keys.
|
||||||
|
func (r *keyring) Signers() ([]ssh.Signer, error) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if r.locked {
|
||||||
|
return nil, errLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
r.expireKeysLocked()
|
||||||
|
s := make([]ssh.Signer, 0, len(r.keys))
|
||||||
|
for _, k := range r.keys {
|
||||||
|
s = append(s, k.signer)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
+451
@@ -0,0 +1,451 @@
|
|||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/dsa"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rsa"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server wraps an Agent and uses it to implement the agent side of
|
||||||
|
// the SSH-agent, wire protocol.
|
||||||
|
type server struct {
|
||||||
|
agent Agent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) processRequestBytes(reqData []byte) []byte {
|
||||||
|
rep, err := s.processRequest(reqData)
|
||||||
|
if err != nil {
|
||||||
|
if err != errLocked {
|
||||||
|
// TODO(hanwen): provide better logging interface?
|
||||||
|
log.Printf("agent %d: %v", reqData[0], err)
|
||||||
|
}
|
||||||
|
return []byte{agentFailure}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil && rep == nil {
|
||||||
|
return []byte{agentSuccess}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ssh.Marshal(rep)
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalKey(k *Key) []byte {
|
||||||
|
var record struct {
|
||||||
|
Blob []byte
|
||||||
|
Comment string
|
||||||
|
}
|
||||||
|
record.Blob = k.Marshal()
|
||||||
|
record.Comment = k.Comment
|
||||||
|
|
||||||
|
return ssh.Marshal(&record)
|
||||||
|
}
|
||||||
|
|
||||||
|
// See [PROTOCOL.agent], section 2.5.1.
|
||||||
|
const agentV1IdentitiesAnswer = 2
|
||||||
|
|
||||||
|
type agentV1IdentityMsg struct {
|
||||||
|
Numkeys uint32 `sshtype:"2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type agentRemoveIdentityMsg struct {
|
||||||
|
KeyBlob []byte `sshtype:"18"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type agentLockMsg struct {
|
||||||
|
Passphrase []byte `sshtype:"22"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type agentUnlockMsg struct {
|
||||||
|
Passphrase []byte `sshtype:"23"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) processRequest(data []byte) (interface{}, error) {
|
||||||
|
switch data[0] {
|
||||||
|
case agentRequestV1Identities:
|
||||||
|
return &agentV1IdentityMsg{0}, nil
|
||||||
|
|
||||||
|
case agentRemoveAllV1Identities:
|
||||||
|
return nil, nil
|
||||||
|
|
||||||
|
case agentRemoveIdentity:
|
||||||
|
var req agentRemoveIdentityMsg
|
||||||
|
if err := ssh.Unmarshal(data, &req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var wk wireKey
|
||||||
|
if err := ssh.Unmarshal(req.KeyBlob, &wk); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, s.agent.Remove(&Key{Format: wk.Format, Blob: req.KeyBlob})
|
||||||
|
|
||||||
|
case agentRemoveAllIdentities:
|
||||||
|
return nil, s.agent.RemoveAll()
|
||||||
|
|
||||||
|
case agentLock:
|
||||||
|
var req agentLockMsg
|
||||||
|
if err := ssh.Unmarshal(data, &req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, s.agent.Lock(req.Passphrase)
|
||||||
|
|
||||||
|
case agentUnlock:
|
||||||
|
var req agentLockMsg
|
||||||
|
if err := ssh.Unmarshal(data, &req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, s.agent.Unlock(req.Passphrase)
|
||||||
|
|
||||||
|
case agentSignRequest:
|
||||||
|
var req signRequestAgentMsg
|
||||||
|
if err := ssh.Unmarshal(data, &req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var wk wireKey
|
||||||
|
if err := ssh.Unmarshal(req.KeyBlob, &wk); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
k := &Key{
|
||||||
|
Format: wk.Format,
|
||||||
|
Blob: req.KeyBlob,
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, err := s.agent.Sign(k, req.Data) // TODO(hanwen): flags.
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &signResponseAgentMsg{SigBlob: ssh.Marshal(sig)}, nil
|
||||||
|
|
||||||
|
case agentRequestIdentities:
|
||||||
|
keys, err := s.agent.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rep := identitiesAnswerAgentMsg{
|
||||||
|
NumKeys: uint32(len(keys)),
|
||||||
|
}
|
||||||
|
for _, k := range keys {
|
||||||
|
rep.Keys = append(rep.Keys, marshalKey(k)...)
|
||||||
|
}
|
||||||
|
return rep, nil
|
||||||
|
|
||||||
|
case agentAddIdConstrained, agentAddIdentity:
|
||||||
|
return nil, s.insertIdentity(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unknown opcode %d", data[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRSAKey(req []byte) (*AddedKey, error) {
|
||||||
|
var k rsaKeyMsg
|
||||||
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if k.E.BitLen() > 30 {
|
||||||
|
return nil, errors.New("agent: RSA public exponent too large")
|
||||||
|
}
|
||||||
|
priv := &rsa.PrivateKey{
|
||||||
|
PublicKey: rsa.PublicKey{
|
||||||
|
E: int(k.E.Int64()),
|
||||||
|
N: k.N,
|
||||||
|
},
|
||||||
|
D: k.D,
|
||||||
|
Primes: []*big.Int{k.P, k.Q},
|
||||||
|
}
|
||||||
|
priv.Precompute()
|
||||||
|
|
||||||
|
return &AddedKey{PrivateKey: priv, Comment: k.Comments}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEd25519Key(req []byte) (*AddedKey, error) {
|
||||||
|
var k ed25519KeyMsg
|
||||||
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
priv := ed25519.PrivateKey(k.Priv)
|
||||||
|
return &AddedKey{PrivateKey: &priv, Comment: k.Comments}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDSAKey(req []byte) (*AddedKey, error) {
|
||||||
|
var k dsaKeyMsg
|
||||||
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
priv := &dsa.PrivateKey{
|
||||||
|
PublicKey: dsa.PublicKey{
|
||||||
|
Parameters: dsa.Parameters{
|
||||||
|
P: k.P,
|
||||||
|
Q: k.Q,
|
||||||
|
G: k.G,
|
||||||
|
},
|
||||||
|
Y: k.Y,
|
||||||
|
},
|
||||||
|
X: k.X,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AddedKey{PrivateKey: priv, Comment: k.Comments}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalECDSA(curveName string, keyBytes []byte, privScalar *big.Int) (priv *ecdsa.PrivateKey, err error) {
|
||||||
|
priv = &ecdsa.PrivateKey{
|
||||||
|
D: privScalar,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch curveName {
|
||||||
|
case "nistp256":
|
||||||
|
priv.Curve = elliptic.P256()
|
||||||
|
case "nistp384":
|
||||||
|
priv.Curve = elliptic.P384()
|
||||||
|
case "nistp521":
|
||||||
|
priv.Curve = elliptic.P521()
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("agent: unknown curve %q", curveName)
|
||||||
|
}
|
||||||
|
|
||||||
|
priv.X, priv.Y = elliptic.Unmarshal(priv.Curve, keyBytes)
|
||||||
|
if priv.X == nil || priv.Y == nil {
|
||||||
|
return nil, errors.New("agent: point not on curve")
|
||||||
|
}
|
||||||
|
|
||||||
|
return priv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEd25519Cert(req []byte) (*AddedKey, error) {
|
||||||
|
var k ed25519CertMsg
|
||||||
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pubKey, err := ssh.ParsePublicKey(k.CertBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
priv := ed25519.PrivateKey(k.Priv)
|
||||||
|
cert, ok := pubKey.(*ssh.Certificate)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("agent: bad ED25519 certificate")
|
||||||
|
}
|
||||||
|
return &AddedKey{PrivateKey: &priv, Certificate: cert, Comment: k.Comments}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseECDSAKey(req []byte) (*AddedKey, error) {
|
||||||
|
var k ecdsaKeyMsg
|
||||||
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, err := unmarshalECDSA(k.Curve, k.KeyBytes, k.D)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AddedKey{PrivateKey: priv, Comment: k.Comments}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRSACert(req []byte) (*AddedKey, error) {
|
||||||
|
var k rsaCertMsg
|
||||||
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey, err := ssh.ParsePublicKey(k.CertBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, ok := pubKey.(*ssh.Certificate)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("agent: bad RSA certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
// An RSA publickey as marshaled by rsaPublicKey.Marshal() in keys.go
|
||||||
|
var rsaPub struct {
|
||||||
|
Name string
|
||||||
|
E *big.Int
|
||||||
|
N *big.Int
|
||||||
|
}
|
||||||
|
if err := ssh.Unmarshal(cert.Key.Marshal(), &rsaPub); err != nil {
|
||||||
|
return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rsaPub.E.BitLen() > 30 {
|
||||||
|
return nil, errors.New("agent: RSA public exponent too large")
|
||||||
|
}
|
||||||
|
|
||||||
|
priv := rsa.PrivateKey{
|
||||||
|
PublicKey: rsa.PublicKey{
|
||||||
|
E: int(rsaPub.E.Int64()),
|
||||||
|
N: rsaPub.N,
|
||||||
|
},
|
||||||
|
D: k.D,
|
||||||
|
Primes: []*big.Int{k.Q, k.P},
|
||||||
|
}
|
||||||
|
priv.Precompute()
|
||||||
|
|
||||||
|
return &AddedKey{PrivateKey: &priv, Certificate: cert, Comment: k.Comments}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDSACert(req []byte) (*AddedKey, error) {
|
||||||
|
var k dsaCertMsg
|
||||||
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pubKey, err := ssh.ParsePublicKey(k.CertBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cert, ok := pubKey.(*ssh.Certificate)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("agent: bad DSA certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A DSA publickey as marshaled by dsaPublicKey.Marshal() in keys.go
|
||||||
|
var w struct {
|
||||||
|
Name string
|
||||||
|
P, Q, G, Y *big.Int
|
||||||
|
}
|
||||||
|
if err := ssh.Unmarshal(cert.Key.Marshal(), &w); err != nil {
|
||||||
|
return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
priv := &dsa.PrivateKey{
|
||||||
|
PublicKey: dsa.PublicKey{
|
||||||
|
Parameters: dsa.Parameters{
|
||||||
|
P: w.P,
|
||||||
|
Q: w.Q,
|
||||||
|
G: w.G,
|
||||||
|
},
|
||||||
|
Y: w.Y,
|
||||||
|
},
|
||||||
|
X: k.X,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AddedKey{PrivateKey: priv, Certificate: cert, Comment: k.Comments}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseECDSACert(req []byte) (*AddedKey, error) {
|
||||||
|
var k ecdsaCertMsg
|
||||||
|
if err := ssh.Unmarshal(req, &k); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey, err := ssh.ParsePublicKey(k.CertBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cert, ok := pubKey.(*ssh.Certificate)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("agent: bad ECDSA certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
// An ECDSA publickey as marshaled by ecdsaPublicKey.Marshal() in keys.go
|
||||||
|
var ecdsaPub struct {
|
||||||
|
Name string
|
||||||
|
ID string
|
||||||
|
Key []byte
|
||||||
|
}
|
||||||
|
if err := ssh.Unmarshal(cert.Key.Marshal(), &ecdsaPub); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, err := unmarshalECDSA(ecdsaPub.ID, ecdsaPub.Key, k.D)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AddedKey{PrivateKey: priv, Certificate: cert, Comment: k.Comments}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) insertIdentity(req []byte) error {
|
||||||
|
var record struct {
|
||||||
|
Type string `sshtype:"17|25"`
|
||||||
|
Rest []byte `ssh:"rest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ssh.Unmarshal(req, &record); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var addedKey *AddedKey
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch record.Type {
|
||||||
|
case ssh.KeyAlgoRSA:
|
||||||
|
addedKey, err = parseRSAKey(req)
|
||||||
|
case ssh.KeyAlgoDSA:
|
||||||
|
addedKey, err = parseDSAKey(req)
|
||||||
|
case ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521:
|
||||||
|
addedKey, err = parseECDSAKey(req)
|
||||||
|
case ssh.KeyAlgoED25519:
|
||||||
|
addedKey, err = parseEd25519Key(req)
|
||||||
|
case ssh.CertAlgoRSAv01:
|
||||||
|
addedKey, err = parseRSACert(req)
|
||||||
|
case ssh.CertAlgoDSAv01:
|
||||||
|
addedKey, err = parseDSACert(req)
|
||||||
|
case ssh.CertAlgoECDSA256v01, ssh.CertAlgoECDSA384v01, ssh.CertAlgoECDSA521v01:
|
||||||
|
addedKey, err = parseECDSACert(req)
|
||||||
|
case ssh.CertAlgoED25519v01:
|
||||||
|
addedKey, err = parseEd25519Cert(req)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("agent: not implemented: %q", record.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.agent.Add(*addedKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeAgent serves the agent protocol on the given connection. It
|
||||||
|
// returns when an I/O error occurs.
|
||||||
|
func ServeAgent(agent Agent, c io.ReadWriter) error {
|
||||||
|
s := &server{agent}
|
||||||
|
|
||||||
|
var length [4]byte
|
||||||
|
for {
|
||||||
|
if _, err := io.ReadFull(c, length[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
l := binary.BigEndian.Uint32(length[:])
|
||||||
|
if l > maxAgentResponseBytes {
|
||||||
|
// We also cap requests.
|
||||||
|
return fmt.Errorf("agent: request too large: %d", l)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := make([]byte, l)
|
||||||
|
if _, err := io.ReadFull(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
repData := s.processRequestBytes(req)
|
||||||
|
if len(repData) > maxAgentResponseBytes {
|
||||||
|
return fmt.Errorf("agent: reply too large: %d bytes", len(repData))
|
||||||
|
}
|
||||||
|
|
||||||
|
binary.BigEndian.PutUint32(length[:], uint32(len(repData)))
|
||||||
|
if _, err := c.Write(length[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := c.Write(repData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -268,7 +268,7 @@ type CertChecker struct {
|
|||||||
// HostKeyFallback is called when CertChecker.CheckHostKey encounters a
|
// HostKeyFallback is called when CertChecker.CheckHostKey encounters a
|
||||||
// public key that is not a certificate. It must implement host key
|
// public key that is not a certificate. It must implement host key
|
||||||
// validation or else, if nil, all such keys are rejected.
|
// validation or else, if nil, all such keys are rejected.
|
||||||
HostKeyFallback func(addr string, remote net.Addr, key PublicKey) error
|
HostKeyFallback HostKeyCallback
|
||||||
|
|
||||||
// IsRevoked is called for each certificate so that revocation checking
|
// IsRevoked is called for each certificate so that revocation checking
|
||||||
// can be implemented. It should return true if the given certificate
|
// can be implemented. It should return true if the given certificate
|
||||||
|
|||||||
+51
-5
@@ -5,6 +5,7 @@
|
|||||||
package ssh
|
package ssh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -13,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Client implements a traditional SSH client that supports shells,
|
// Client implements a traditional SSH client that supports shells,
|
||||||
// subprocesses, port forwarding and tunneled dialing.
|
// subprocesses, TCP port/streamlocal forwarding and tunneled dialing.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
Conn
|
Conn
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ func NewClient(c Conn, chans <-chan NewChannel, reqs <-chan *Request) *Client {
|
|||||||
conn.forwards.closeAll()
|
conn.forwards.closeAll()
|
||||||
}()
|
}()
|
||||||
go conn.forwards.handleChannels(conn.HandleChannelOpen("forwarded-tcpip"))
|
go conn.forwards.handleChannels(conn.HandleChannelOpen("forwarded-tcpip"))
|
||||||
|
go conn.forwards.handleChannels(conn.HandleChannelOpen("forwarded-streamlocal@openssh.com"))
|
||||||
return conn
|
return conn
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +70,11 @@ func NewClient(c Conn, chans <-chan NewChannel, reqs <-chan *Request) *Client {
|
|||||||
func NewClientConn(c net.Conn, addr string, config *ClientConfig) (Conn, <-chan NewChannel, <-chan *Request, error) {
|
func NewClientConn(c net.Conn, addr string, config *ClientConfig) (Conn, <-chan NewChannel, <-chan *Request, error) {
|
||||||
fullConf := *config
|
fullConf := *config
|
||||||
fullConf.SetDefaults()
|
fullConf.SetDefaults()
|
||||||
|
if fullConf.HostKeyCallback == nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, nil, nil, errors.New("ssh: must specify HostKeyCallback")
|
||||||
|
}
|
||||||
|
|
||||||
conn := &connection{
|
conn := &connection{
|
||||||
sshConn: sshConn{conn: c},
|
sshConn: sshConn{conn: c},
|
||||||
}
|
}
|
||||||
@@ -173,6 +180,13 @@ func Dial(network, addr string, config *ClientConfig) (*Client, error) {
|
|||||||
return NewClient(c, chans, reqs), nil
|
return NewClient(c, chans, reqs), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HostKeyCallback is the function type used for verifying server
|
||||||
|
// keys. A HostKeyCallback must return nil if the host key is OK, or
|
||||||
|
// an error to reject it. It receives the hostname as passed to Dial
|
||||||
|
// or NewClientConn. The remote address is the RemoteAddr of the
|
||||||
|
// net.Conn underlying the the SSH connection.
|
||||||
|
type HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
|
||||||
|
|
||||||
// A ClientConfig structure is used to configure a Client. It must not be
|
// A ClientConfig structure is used to configure a Client. It must not be
|
||||||
// modified after having been passed to an SSH function.
|
// modified after having been passed to an SSH function.
|
||||||
type ClientConfig struct {
|
type ClientConfig struct {
|
||||||
@@ -188,10 +202,12 @@ type ClientConfig struct {
|
|||||||
// be used during authentication.
|
// be used during authentication.
|
||||||
Auth []AuthMethod
|
Auth []AuthMethod
|
||||||
|
|
||||||
// HostKeyCallback, if not nil, is called during the cryptographic
|
// HostKeyCallback is called during the cryptographic
|
||||||
// handshake to validate the server's host key. A nil HostKeyCallback
|
// handshake to validate the server's host key. The client
|
||||||
// implies that all host keys are accepted.
|
// configuration must supply this callback for the connection
|
||||||
HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
|
// to succeed. The functions InsecureIgnoreHostKey or
|
||||||
|
// FixedHostKey can be used for simplistic host key checks.
|
||||||
|
HostKeyCallback HostKeyCallback
|
||||||
|
|
||||||
// ClientVersion contains the version identification string that will
|
// ClientVersion contains the version identification string that will
|
||||||
// be used for the connection. If empty, a reasonable default is used.
|
// be used for the connection. If empty, a reasonable default is used.
|
||||||
@@ -209,3 +225,33 @@ type ClientConfig struct {
|
|||||||
// A Timeout of zero means no timeout.
|
// A Timeout of zero means no timeout.
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InsecureIgnoreHostKey returns a function that can be used for
|
||||||
|
// ClientConfig.HostKeyCallback to accept any host key. It should
|
||||||
|
// not be used for production code.
|
||||||
|
func InsecureIgnoreHostKey() HostKeyCallback {
|
||||||
|
return func(hostname string, remote net.Addr, key PublicKey) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fixedHostKey struct {
|
||||||
|
key PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fixedHostKey) check(hostname string, remote net.Addr, key PublicKey) error {
|
||||||
|
if f.key == nil {
|
||||||
|
return fmt.Errorf("ssh: required host key was nil")
|
||||||
|
}
|
||||||
|
if !bytes.Equal(key.Marshal(), f.key.Marshal()) {
|
||||||
|
return fmt.Errorf("ssh: host key mismatch")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FixedHostKey returns a function for use in
|
||||||
|
// ClientConfig.HostKeyCallback to accept only a specific host key.
|
||||||
|
func FixedHostKey(key PublicKey) HostKeyCallback {
|
||||||
|
hk := &fixedHostKey{key}
|
||||||
|
return hk.check
|
||||||
|
}
|
||||||
|
|||||||
+30
-19
@@ -179,31 +179,26 @@ func (cb publicKeyCallback) method() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) {
|
func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) {
|
||||||
// Authentication is performed in two stages. The first stage sends an
|
// Authentication is performed by sending an enquiry to test if a key is
|
||||||
// enquiry to test if each key is acceptable to the remote. The second
|
// acceptable to the remote. If the key is acceptable, the client will
|
||||||
// stage attempts to authenticate with the valid keys obtained in the
|
// attempt to authenticate with the valid key. If not the client will repeat
|
||||||
// first stage.
|
// the process with the remaining keys.
|
||||||
|
|
||||||
signers, err := cb()
|
signers, err := cb()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
var validKeys []Signer
|
|
||||||
for _, signer := range signers {
|
|
||||||
if ok, err := validateKey(signer.PublicKey(), user, c); ok {
|
|
||||||
validKeys = append(validKeys, signer)
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// methods that may continue if this auth is not successful.
|
|
||||||
var methods []string
|
var methods []string
|
||||||
for _, signer := range validKeys {
|
for _, signer := range signers {
|
||||||
pub := signer.PublicKey()
|
ok, err := validateKey(signer.PublicKey(), user, c)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pub := signer.PublicKey()
|
||||||
pubKey := pub.Marshal()
|
pubKey := pub.Marshal()
|
||||||
sign, err := signer.Sign(rand, buildDataSignedForAuth(session, userAuthRequestMsg{
|
sign, err := signer.Sign(rand, buildDataSignedForAuth(session, userAuthRequestMsg{
|
||||||
User: user,
|
User: user,
|
||||||
@@ -236,13 +231,29 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
if success {
|
|
||||||
|
// If authentication succeeds or the list of available methods does not
|
||||||
|
// contain the "publickey" method, do not attempt to authenticate with any
|
||||||
|
// other keys. According to RFC 4252 Section 7, the latter can occur when
|
||||||
|
// additional authentication methods are required.
|
||||||
|
if success || !containsMethod(methods, cb.method()) {
|
||||||
return success, methods, err
|
return success, methods, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, methods, nil
|
return false, methods, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func containsMethod(methods []string, method string) bool {
|
||||||
|
for _, m := range methods {
|
||||||
|
if m == method {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// validateKey validates the key provided is acceptable to the server.
|
// validateKey validates the key provided is acceptable to the server.
|
||||||
func validateKey(key PublicKey, user string, c packetConn) (bool, error) {
|
func validateKey(key PublicKey, user string, c packetConn) (bool, error) {
|
||||||
pubKey := key.Marshal()
|
pubKey := key.Marshal()
|
||||||
|
|||||||
+8
-6
@@ -9,6 +9,7 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
_ "crypto/sha1"
|
_ "crypto/sha1"
|
||||||
@@ -40,7 +41,7 @@ var supportedKexAlgos = []string{
|
|||||||
kexAlgoDH14SHA1, kexAlgoDH1SHA1,
|
kexAlgoDH14SHA1, kexAlgoDH1SHA1,
|
||||||
}
|
}
|
||||||
|
|
||||||
// supportedKexAlgos specifies the supported host-key algorithms (i.e. methods
|
// supportedHostKeyAlgos specifies the supported host-key algorithms (i.e. methods
|
||||||
// of authenticating servers) in preference order.
|
// of authenticating servers) in preference order.
|
||||||
var supportedHostKeyAlgos = []string{
|
var supportedHostKeyAlgos = []string{
|
||||||
CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01,
|
CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01,
|
||||||
@@ -186,7 +187,7 @@ type Config struct {
|
|||||||
|
|
||||||
// The maximum number of bytes sent or received after which a
|
// The maximum number of bytes sent or received after which a
|
||||||
// new key is negotiated. It must be at least 256. If
|
// new key is negotiated. It must be at least 256. If
|
||||||
// unspecified, 1 gigabyte is used.
|
// unspecified, a size suitable for the chosen cipher is used.
|
||||||
RekeyThreshold uint64
|
RekeyThreshold uint64
|
||||||
|
|
||||||
// The allowed key exchanges algorithms. If unspecified then a
|
// The allowed key exchanges algorithms. If unspecified then a
|
||||||
@@ -230,11 +231,12 @@ func (c *Config) SetDefaults() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.RekeyThreshold == 0 {
|
if c.RekeyThreshold == 0 {
|
||||||
// RFC 4253, section 9 suggests rekeying after 1G.
|
// cipher specific default
|
||||||
c.RekeyThreshold = 1 << 30
|
} else if c.RekeyThreshold < minRekeyThreshold {
|
||||||
}
|
|
||||||
if c.RekeyThreshold < minRekeyThreshold {
|
|
||||||
c.RekeyThreshold = minRekeyThreshold
|
c.RekeyThreshold = minRekeyThreshold
|
||||||
|
} else if c.RekeyThreshold >= math.MaxInt64 {
|
||||||
|
// Avoid weirdness if somebody uses -1 as a threshold.
|
||||||
|
c.RekeyThreshold = math.MaxInt64
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
@@ -14,5 +14,8 @@ others.
|
|||||||
References:
|
References:
|
||||||
[PROTOCOL.certkeys]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?rev=HEAD
|
[PROTOCOL.certkeys]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?rev=HEAD
|
||||||
[SSH-PARAMETERS]: http://www.iana.org/assignments/ssh-parameters/ssh-parameters.xml#ssh-parameters-1
|
[SSH-PARAMETERS]: http://www.iana.org/assignments/ssh-parameters/ssh-parameters.xml#ssh-parameters-1
|
||||||
|
|
||||||
|
This package does not fall under the stability promise of the Go language itself,
|
||||||
|
so its API may be changed when pressing needs arise.
|
||||||
*/
|
*/
|
||||||
package ssh // import "golang.org/x/crypto/ssh"
|
package ssh // import "golang.org/x/crypto/ssh"
|
||||||
|
|||||||
+34
-19
@@ -74,7 +74,7 @@ type handshakeTransport struct {
|
|||||||
startKex chan *pendingKex
|
startKex chan *pendingKex
|
||||||
|
|
||||||
// data for host key checking
|
// data for host key checking
|
||||||
hostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
|
hostKeyCallback HostKeyCallback
|
||||||
dialAddress string
|
dialAddress string
|
||||||
remoteAddr net.Addr
|
remoteAddr net.Addr
|
||||||
|
|
||||||
@@ -107,6 +107,8 @@ func newHandshakeTransport(conn keyingTransport, config *Config, clientVersion,
|
|||||||
|
|
||||||
config: config,
|
config: config,
|
||||||
}
|
}
|
||||||
|
t.resetReadThresholds()
|
||||||
|
t.resetWriteThresholds()
|
||||||
|
|
||||||
// We always start with a mandatory key exchange.
|
// We always start with a mandatory key exchange.
|
||||||
t.requestKex <- struct{}{}
|
t.requestKex <- struct{}{}
|
||||||
@@ -237,6 +239,17 @@ func (t *handshakeTransport) requestKeyExchange() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *handshakeTransport) resetWriteThresholds() {
|
||||||
|
t.writePacketsLeft = packetRekeyThreshold
|
||||||
|
if t.config.RekeyThreshold > 0 {
|
||||||
|
t.writeBytesLeft = int64(t.config.RekeyThreshold)
|
||||||
|
} else if t.algorithms != nil {
|
||||||
|
t.writeBytesLeft = t.algorithms.w.rekeyBytes()
|
||||||
|
} else {
|
||||||
|
t.writeBytesLeft = 1 << 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (t *handshakeTransport) kexLoop() {
|
func (t *handshakeTransport) kexLoop() {
|
||||||
|
|
||||||
write:
|
write:
|
||||||
@@ -285,12 +298,8 @@ write:
|
|||||||
t.writeError = err
|
t.writeError = err
|
||||||
t.sentInitPacket = nil
|
t.sentInitPacket = nil
|
||||||
t.sentInitMsg = nil
|
t.sentInitMsg = nil
|
||||||
t.writePacketsLeft = packetRekeyThreshold
|
|
||||||
if t.config.RekeyThreshold > 0 {
|
t.resetWriteThresholds()
|
||||||
t.writeBytesLeft = int64(t.config.RekeyThreshold)
|
|
||||||
} else if t.algorithms != nil {
|
|
||||||
t.writeBytesLeft = t.algorithms.w.rekeyBytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// we have completed the key exchange. Since the
|
// we have completed the key exchange. Since the
|
||||||
// reader is still blocked, it is safe to clear out
|
// reader is still blocked, it is safe to clear out
|
||||||
@@ -344,6 +353,17 @@ write:
|
|||||||
// key exchange itself.
|
// key exchange itself.
|
||||||
const packetRekeyThreshold = (1 << 31)
|
const packetRekeyThreshold = (1 << 31)
|
||||||
|
|
||||||
|
func (t *handshakeTransport) resetReadThresholds() {
|
||||||
|
t.readPacketsLeft = packetRekeyThreshold
|
||||||
|
if t.config.RekeyThreshold > 0 {
|
||||||
|
t.readBytesLeft = int64(t.config.RekeyThreshold)
|
||||||
|
} else if t.algorithms != nil {
|
||||||
|
t.readBytesLeft = t.algorithms.r.rekeyBytes()
|
||||||
|
} else {
|
||||||
|
t.readBytesLeft = 1 << 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (t *handshakeTransport) readOnePacket(first bool) ([]byte, error) {
|
func (t *handshakeTransport) readOnePacket(first bool) ([]byte, error) {
|
||||||
p, err := t.conn.readPacket()
|
p, err := t.conn.readPacket()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -391,12 +411,7 @@ func (t *handshakeTransport) readOnePacket(first bool) ([]byte, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
t.readPacketsLeft = packetRekeyThreshold
|
t.resetReadThresholds()
|
||||||
if t.config.RekeyThreshold > 0 {
|
|
||||||
t.readBytesLeft = int64(t.config.RekeyThreshold)
|
|
||||||
} else {
|
|
||||||
t.readBytesLeft = t.algorithms.r.rekeyBytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// By default, a key exchange is hidden from higher layers by
|
// By default, a key exchange is hidden from higher layers by
|
||||||
// translating it into msgIgnore.
|
// translating it into msgIgnore.
|
||||||
@@ -574,7 +589,9 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error {
|
|||||||
}
|
}
|
||||||
result.SessionID = t.sessionID
|
result.SessionID = t.sessionID
|
||||||
|
|
||||||
t.conn.prepareKeyChange(t.algorithms, result)
|
if err := t.conn.prepareKeyChange(t.algorithms, result); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err = t.conn.writePacket([]byte{msgNewKeys}); err != nil {
|
if err = t.conn.writePacket([]byte{msgNewKeys}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -614,11 +631,9 @@ func (t *handshakeTransport) client(kex kexAlgorithm, algs *algorithms, magics *
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.hostKeyCallback != nil {
|
err = t.hostKeyCallback(t.dialAddress, t.remoteAddr, hostKey)
|
||||||
err = t.hostKeyCallback(t.dialAddress, t.remoteAddr, hostKey)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|||||||
+73
-21
@@ -824,7 +824,7 @@ func ParseDSAPrivateKey(der []byte) (*dsa.PrivateKey, error) {
|
|||||||
|
|
||||||
// Implemented based on the documentation at
|
// Implemented based on the documentation at
|
||||||
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
|
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
|
||||||
func parseOpenSSHPrivateKey(key []byte) (*ed25519.PrivateKey, error) {
|
func parseOpenSSHPrivateKey(key []byte) (crypto.PrivateKey, error) {
|
||||||
magic := append([]byte("openssh-key-v1"), 0)
|
magic := append([]byte("openssh-key-v1"), 0)
|
||||||
if !bytes.Equal(magic, key[0:len(magic)]) {
|
if !bytes.Equal(magic, key[0:len(magic)]) {
|
||||||
return nil, errors.New("ssh: invalid openssh private key format")
|
return nil, errors.New("ssh: invalid openssh private key format")
|
||||||
@@ -844,14 +844,15 @@ func parseOpenSSHPrivateKey(key []byte) (*ed25519.PrivateKey, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if w.KdfName != "none" || w.CipherName != "none" {
|
||||||
|
return nil, errors.New("ssh: cannot decode encrypted private keys")
|
||||||
|
}
|
||||||
|
|
||||||
pk1 := struct {
|
pk1 := struct {
|
||||||
Check1 uint32
|
Check1 uint32
|
||||||
Check2 uint32
|
Check2 uint32
|
||||||
Keytype string
|
Keytype string
|
||||||
Pub []byte
|
Rest []byte `ssh:"rest"`
|
||||||
Priv []byte
|
|
||||||
Comment string
|
|
||||||
Pad []byte `ssh:"rest"`
|
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
if err := Unmarshal(w.PrivKeyBlock, &pk1); err != nil {
|
if err := Unmarshal(w.PrivKeyBlock, &pk1); err != nil {
|
||||||
@@ -862,24 +863,75 @@ func parseOpenSSHPrivateKey(key []byte) (*ed25519.PrivateKey, error) {
|
|||||||
return nil, errors.New("ssh: checkint mismatch")
|
return nil, errors.New("ssh: checkint mismatch")
|
||||||
}
|
}
|
||||||
|
|
||||||
// we only handle ed25519 keys currently
|
// we only handle ed25519 and rsa keys currently
|
||||||
if pk1.Keytype != KeyAlgoED25519 {
|
switch pk1.Keytype {
|
||||||
|
case KeyAlgoRSA:
|
||||||
|
// https://github.com/openssh/openssh-portable/blob/master/sshkey.c#L2760-L2773
|
||||||
|
key := struct {
|
||||||
|
N *big.Int
|
||||||
|
E *big.Int
|
||||||
|
D *big.Int
|
||||||
|
Iqmp *big.Int
|
||||||
|
P *big.Int
|
||||||
|
Q *big.Int
|
||||||
|
Comment string
|
||||||
|
Pad []byte `ssh:"rest"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
if err := Unmarshal(pk1.Rest, &key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, b := range key.Pad {
|
||||||
|
if int(b) != i+1 {
|
||||||
|
return nil, errors.New("ssh: padding not as expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pk := &rsa.PrivateKey{
|
||||||
|
PublicKey: rsa.PublicKey{
|
||||||
|
N: key.N,
|
||||||
|
E: int(key.E.Int64()),
|
||||||
|
},
|
||||||
|
D: key.D,
|
||||||
|
Primes: []*big.Int{key.P, key.Q},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pk.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pk.Precompute()
|
||||||
|
|
||||||
|
return pk, nil
|
||||||
|
case KeyAlgoED25519:
|
||||||
|
key := struct {
|
||||||
|
Pub []byte
|
||||||
|
Priv []byte
|
||||||
|
Comment string
|
||||||
|
Pad []byte `ssh:"rest"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
if err := Unmarshal(pk1.Rest, &key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(key.Priv) != ed25519.PrivateKeySize {
|
||||||
|
return nil, errors.New("ssh: private key unexpected length")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, b := range key.Pad {
|
||||||
|
if int(b) != i+1 {
|
||||||
|
return nil, errors.New("ssh: padding not as expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pk := ed25519.PrivateKey(make([]byte, ed25519.PrivateKeySize))
|
||||||
|
copy(pk, key.Priv)
|
||||||
|
return &pk, nil
|
||||||
|
default:
|
||||||
return nil, errors.New("ssh: unhandled key type")
|
return nil, errors.New("ssh: unhandled key type")
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, b := range pk1.Pad {
|
|
||||||
if int(b) != i+1 {
|
|
||||||
return nil, errors.New("ssh: padding not as expected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pk1.Priv) != ed25519.PrivateKeySize {
|
|
||||||
return nil, errors.New("ssh: private key unexpected length")
|
|
||||||
}
|
|
||||||
|
|
||||||
pk := ed25519.PrivateKey(make([]byte, ed25519.PrivateKeySize))
|
|
||||||
copy(pk, pk1.Priv)
|
|
||||||
return &pk, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FingerprintLegacyMD5 returns the user presentation of the key's
|
// FingerprintLegacyMD5 returns the user presentation of the key's
|
||||||
|
|||||||
+33
@@ -45,6 +45,12 @@ type ServerConfig struct {
|
|||||||
// authenticating.
|
// authenticating.
|
||||||
NoClientAuth bool
|
NoClientAuth bool
|
||||||
|
|
||||||
|
// MaxAuthTries specifies the maximum number of authentication attempts
|
||||||
|
// permitted per connection. If set to a negative number, the number of
|
||||||
|
// attempts are unlimited. If set to zero, the number of attempts are limited
|
||||||
|
// to 6.
|
||||||
|
MaxAuthTries int
|
||||||
|
|
||||||
// PasswordCallback, if non-nil, is called when a user
|
// PasswordCallback, if non-nil, is called when a user
|
||||||
// attempts to authenticate using a password.
|
// attempts to authenticate using a password.
|
||||||
PasswordCallback func(conn ConnMetadata, password []byte) (*Permissions, error)
|
PasswordCallback func(conn ConnMetadata, password []byte) (*Permissions, error)
|
||||||
@@ -141,6 +147,10 @@ type ServerConn struct {
|
|||||||
// Request and NewChannel channels must be serviced, or the connection
|
// Request and NewChannel channels must be serviced, or the connection
|
||||||
// will hang.
|
// will hang.
|
||||||
func NewServerConn(c net.Conn, config *ServerConfig) (*ServerConn, <-chan NewChannel, <-chan *Request, error) {
|
func NewServerConn(c net.Conn, config *ServerConfig) (*ServerConn, <-chan NewChannel, <-chan *Request, error) {
|
||||||
|
if config.MaxAuthTries == 0 {
|
||||||
|
config.MaxAuthTries = 6
|
||||||
|
}
|
||||||
|
|
||||||
fullConf := *config
|
fullConf := *config
|
||||||
fullConf.SetDefaults()
|
fullConf.SetDefaults()
|
||||||
s := &connection{
|
s := &connection{
|
||||||
@@ -267,8 +277,23 @@ func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, err
|
|||||||
var cache pubKeyCache
|
var cache pubKeyCache
|
||||||
var perms *Permissions
|
var perms *Permissions
|
||||||
|
|
||||||
|
authFailures := 0
|
||||||
|
|
||||||
userAuthLoop:
|
userAuthLoop:
|
||||||
for {
|
for {
|
||||||
|
if authFailures >= config.MaxAuthTries && config.MaxAuthTries > 0 {
|
||||||
|
discMsg := &disconnectMsg{
|
||||||
|
Reason: 2,
|
||||||
|
Message: "too many authentication failures",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.transport.writePacket(Marshal(discMsg)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, discMsg
|
||||||
|
}
|
||||||
|
|
||||||
var userAuthReq userAuthRequestMsg
|
var userAuthReq userAuthRequestMsg
|
||||||
if packet, err := s.transport.readPacket(); err != nil {
|
if packet, err := s.transport.readPacket(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -289,6 +314,11 @@ userAuthLoop:
|
|||||||
if config.NoClientAuth {
|
if config.NoClientAuth {
|
||||||
authErr = nil
|
authErr = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allow initial attempt of 'none' without penalty
|
||||||
|
if authFailures == 0 {
|
||||||
|
authFailures--
|
||||||
|
}
|
||||||
case "password":
|
case "password":
|
||||||
if config.PasswordCallback == nil {
|
if config.PasswordCallback == nil {
|
||||||
authErr = errors.New("ssh: password auth not configured")
|
authErr = errors.New("ssh: password auth not configured")
|
||||||
@@ -360,6 +390,7 @@ userAuthLoop:
|
|||||||
if isQuery {
|
if isQuery {
|
||||||
// The client can query if the given public key
|
// The client can query if the given public key
|
||||||
// would be okay.
|
// would be okay.
|
||||||
|
|
||||||
if len(payload) > 0 {
|
if len(payload) > 0 {
|
||||||
return nil, parseError(msgUserAuthRequest)
|
return nil, parseError(msgUserAuthRequest)
|
||||||
}
|
}
|
||||||
@@ -409,6 +440,8 @@ userAuthLoop:
|
|||||||
break userAuthLoop
|
break userAuthLoop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authFailures++
|
||||||
|
|
||||||
var failureMsg userAuthFailureMsg
|
var failureMsg userAuthFailureMsg
|
||||||
if config.PasswordCallback != nil {
|
if config.PasswordCallback != nil {
|
||||||
failureMsg.Methods = append(failureMsg.Methods, "password")
|
failureMsg.Methods = append(failureMsg.Methods, "password")
|
||||||
|
|||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// streamLocalChannelOpenDirectMsg is a struct used for SSH_MSG_CHANNEL_OPEN message
|
||||||
|
// with "direct-streamlocal@openssh.com" string.
|
||||||
|
//
|
||||||
|
// See openssh-portable/PROTOCOL, section 2.4. connection: Unix domain socket forwarding
|
||||||
|
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL#L235
|
||||||
|
type streamLocalChannelOpenDirectMsg struct {
|
||||||
|
socketPath string
|
||||||
|
reserved0 string
|
||||||
|
reserved1 uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// forwardedStreamLocalPayload is a struct used for SSH_MSG_CHANNEL_OPEN message
|
||||||
|
// with "forwarded-streamlocal@openssh.com" string.
|
||||||
|
type forwardedStreamLocalPayload struct {
|
||||||
|
SocketPath string
|
||||||
|
Reserved0 string
|
||||||
|
}
|
||||||
|
|
||||||
|
// streamLocalChannelForwardMsg is a struct used for SSH2_MSG_GLOBAL_REQUEST message
|
||||||
|
// with "streamlocal-forward@openssh.com"/"cancel-streamlocal-forward@openssh.com" string.
|
||||||
|
type streamLocalChannelForwardMsg struct {
|
||||||
|
socketPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenUnix is similar to ListenTCP but uses a Unix domain socket.
|
||||||
|
func (c *Client) ListenUnix(socketPath string) (net.Listener, error) {
|
||||||
|
m := streamLocalChannelForwardMsg{
|
||||||
|
socketPath,
|
||||||
|
}
|
||||||
|
// send message
|
||||||
|
ok, _, err := c.SendRequest("streamlocal-forward@openssh.com", true, Marshal(&m))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("ssh: streamlocal-forward@openssh.com request denied by peer")
|
||||||
|
}
|
||||||
|
ch := c.forwards.add(&net.UnixAddr{Name: socketPath, Net: "unix"})
|
||||||
|
|
||||||
|
return &unixListener{socketPath, c, ch}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) dialStreamLocal(socketPath string) (Channel, error) {
|
||||||
|
msg := streamLocalChannelOpenDirectMsg{
|
||||||
|
socketPath: socketPath,
|
||||||
|
}
|
||||||
|
ch, in, err := c.OpenChannel("direct-streamlocal@openssh.com", Marshal(&msg))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
go DiscardRequests(in)
|
||||||
|
return ch, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type unixListener struct {
|
||||||
|
socketPath string
|
||||||
|
|
||||||
|
conn *Client
|
||||||
|
in <-chan forward
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept waits for and returns the next connection to the listener.
|
||||||
|
func (l *unixListener) Accept() (net.Conn, error) {
|
||||||
|
s, ok := <-l.in
|
||||||
|
if !ok {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
ch, incoming, err := s.newCh.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
go DiscardRequests(incoming)
|
||||||
|
|
||||||
|
return &chanConn{
|
||||||
|
Channel: ch,
|
||||||
|
laddr: &net.UnixAddr{
|
||||||
|
Name: l.socketPath,
|
||||||
|
Net: "unix",
|
||||||
|
},
|
||||||
|
raddr: &net.UnixAddr{
|
||||||
|
Name: "@",
|
||||||
|
Net: "unix",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the listener.
|
||||||
|
func (l *unixListener) Close() error {
|
||||||
|
// this also closes the listener.
|
||||||
|
l.conn.forwards.remove(&net.UnixAddr{Name: l.socketPath, Net: "unix"})
|
||||||
|
m := streamLocalChannelForwardMsg{
|
||||||
|
l.socketPath,
|
||||||
|
}
|
||||||
|
ok, _, err := l.conn.SendRequest("cancel-streamlocal-forward@openssh.com", true, Marshal(&m))
|
||||||
|
if err == nil && !ok {
|
||||||
|
err = errors.New("ssh: cancel-streamlocal-forward@openssh.com failed")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addr returns the listener's network address.
|
||||||
|
func (l *unixListener) Addr() net.Addr {
|
||||||
|
return &net.UnixAddr{
|
||||||
|
Name: l.socketPath,
|
||||||
|
Net: "unix",
|
||||||
|
}
|
||||||
|
}
|
||||||
+127
-69
@@ -20,12 +20,20 @@ import (
|
|||||||
// addr. Incoming connections will be available by calling Accept on
|
// addr. Incoming connections will be available by calling Accept on
|
||||||
// the returned net.Listener. The listener must be serviced, or the
|
// the returned net.Listener. The listener must be serviced, or the
|
||||||
// SSH connection may hang.
|
// SSH connection may hang.
|
||||||
|
// N must be "tcp", "tcp4", "tcp6", or "unix".
|
||||||
func (c *Client) Listen(n, addr string) (net.Listener, error) {
|
func (c *Client) Listen(n, addr string) (net.Listener, error) {
|
||||||
laddr, err := net.ResolveTCPAddr(n, addr)
|
switch n {
|
||||||
if err != nil {
|
case "tcp", "tcp4", "tcp6":
|
||||||
return nil, err
|
laddr, err := net.ResolveTCPAddr(n, addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.ListenTCP(laddr)
|
||||||
|
case "unix":
|
||||||
|
return c.ListenUnix(addr)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("ssh: unsupported protocol: %s", n)
|
||||||
}
|
}
|
||||||
return c.ListenTCP(laddr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatic port allocation is broken with OpenSSH before 6.0. See
|
// Automatic port allocation is broken with OpenSSH before 6.0. See
|
||||||
@@ -116,7 +124,7 @@ func (c *Client) ListenTCP(laddr *net.TCPAddr) (net.Listener, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register this forward, using the port number we obtained.
|
// Register this forward, using the port number we obtained.
|
||||||
ch := c.forwards.add(*laddr)
|
ch := c.forwards.add(laddr)
|
||||||
|
|
||||||
return &tcpListener{laddr, c, ch}, nil
|
return &tcpListener{laddr, c, ch}, nil
|
||||||
}
|
}
|
||||||
@@ -131,7 +139,7 @@ type forwardList struct {
|
|||||||
// forwardEntry represents an established mapping of a laddr on a
|
// forwardEntry represents an established mapping of a laddr on a
|
||||||
// remote ssh server to a channel connected to a tcpListener.
|
// remote ssh server to a channel connected to a tcpListener.
|
||||||
type forwardEntry struct {
|
type forwardEntry struct {
|
||||||
laddr net.TCPAddr
|
laddr net.Addr
|
||||||
c chan forward
|
c chan forward
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,16 +147,16 @@ type forwardEntry struct {
|
|||||||
// arguments to add/remove/lookup should be address as specified in
|
// arguments to add/remove/lookup should be address as specified in
|
||||||
// the original forward-request.
|
// the original forward-request.
|
||||||
type forward struct {
|
type forward struct {
|
||||||
newCh NewChannel // the ssh client channel underlying this forward
|
newCh NewChannel // the ssh client channel underlying this forward
|
||||||
raddr *net.TCPAddr // the raddr of the incoming connection
|
raddr net.Addr // the raddr of the incoming connection
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *forwardList) add(addr net.TCPAddr) chan forward {
|
func (l *forwardList) add(addr net.Addr) chan forward {
|
||||||
l.Lock()
|
l.Lock()
|
||||||
defer l.Unlock()
|
defer l.Unlock()
|
||||||
f := forwardEntry{
|
f := forwardEntry{
|
||||||
addr,
|
laddr: addr,
|
||||||
make(chan forward, 1),
|
c: make(chan forward, 1),
|
||||||
}
|
}
|
||||||
l.entries = append(l.entries, f)
|
l.entries = append(l.entries, f)
|
||||||
return f.c
|
return f.c
|
||||||
@@ -176,44 +184,69 @@ func parseTCPAddr(addr string, port uint32) (*net.TCPAddr, error) {
|
|||||||
|
|
||||||
func (l *forwardList) handleChannels(in <-chan NewChannel) {
|
func (l *forwardList) handleChannels(in <-chan NewChannel) {
|
||||||
for ch := range in {
|
for ch := range in {
|
||||||
var payload forwardedTCPPayload
|
var (
|
||||||
if err := Unmarshal(ch.ExtraData(), &payload); err != nil {
|
laddr net.Addr
|
||||||
ch.Reject(ConnectionFailed, "could not parse forwarded-tcpip payload: "+err.Error())
|
raddr net.Addr
|
||||||
continue
|
err error
|
||||||
}
|
)
|
||||||
|
switch channelType := ch.ChannelType(); channelType {
|
||||||
|
case "forwarded-tcpip":
|
||||||
|
var payload forwardedTCPPayload
|
||||||
|
if err = Unmarshal(ch.ExtraData(), &payload); err != nil {
|
||||||
|
ch.Reject(ConnectionFailed, "could not parse forwarded-tcpip payload: "+err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// RFC 4254 section 7.2 specifies that incoming
|
// RFC 4254 section 7.2 specifies that incoming
|
||||||
// addresses should list the address, in string
|
// addresses should list the address, in string
|
||||||
// format. It is implied that this should be an IP
|
// format. It is implied that this should be an IP
|
||||||
// address, as it would be impossible to connect to it
|
// address, as it would be impossible to connect to it
|
||||||
// otherwise.
|
// otherwise.
|
||||||
laddr, err := parseTCPAddr(payload.Addr, payload.Port)
|
laddr, err = parseTCPAddr(payload.Addr, payload.Port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ch.Reject(ConnectionFailed, err.Error())
|
ch.Reject(ConnectionFailed, err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
raddr, err := parseTCPAddr(payload.OriginAddr, payload.OriginPort)
|
raddr, err = parseTCPAddr(payload.OriginAddr, payload.OriginPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ch.Reject(ConnectionFailed, err.Error())
|
ch.Reject(ConnectionFailed, err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok := l.forward(*laddr, *raddr, ch); !ok {
|
case "forwarded-streamlocal@openssh.com":
|
||||||
|
var payload forwardedStreamLocalPayload
|
||||||
|
if err = Unmarshal(ch.ExtraData(), &payload); err != nil {
|
||||||
|
ch.Reject(ConnectionFailed, "could not parse forwarded-streamlocal@openssh.com payload: "+err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
laddr = &net.UnixAddr{
|
||||||
|
Name: payload.SocketPath,
|
||||||
|
Net: "unix",
|
||||||
|
}
|
||||||
|
raddr = &net.UnixAddr{
|
||||||
|
Name: "@",
|
||||||
|
Net: "unix",
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("ssh: unknown channel type %s", channelType))
|
||||||
|
}
|
||||||
|
if ok := l.forward(laddr, raddr, ch); !ok {
|
||||||
// Section 7.2, implementations MUST reject spurious incoming
|
// Section 7.2, implementations MUST reject spurious incoming
|
||||||
// connections.
|
// connections.
|
||||||
ch.Reject(Prohibited, "no forward for address")
|
ch.Reject(Prohibited, "no forward for address")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove removes the forward entry, and the channel feeding its
|
// remove removes the forward entry, and the channel feeding its
|
||||||
// listener.
|
// listener.
|
||||||
func (l *forwardList) remove(addr net.TCPAddr) {
|
func (l *forwardList) remove(addr net.Addr) {
|
||||||
l.Lock()
|
l.Lock()
|
||||||
defer l.Unlock()
|
defer l.Unlock()
|
||||||
for i, f := range l.entries {
|
for i, f := range l.entries {
|
||||||
if addr.IP.Equal(f.laddr.IP) && addr.Port == f.laddr.Port {
|
if addr.Network() == f.laddr.Network() && addr.String() == f.laddr.String() {
|
||||||
l.entries = append(l.entries[:i], l.entries[i+1:]...)
|
l.entries = append(l.entries[:i], l.entries[i+1:]...)
|
||||||
close(f.c)
|
close(f.c)
|
||||||
return
|
return
|
||||||
@@ -231,12 +264,12 @@ func (l *forwardList) closeAll() {
|
|||||||
l.entries = nil
|
l.entries = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *forwardList) forward(laddr, raddr net.TCPAddr, ch NewChannel) bool {
|
func (l *forwardList) forward(laddr, raddr net.Addr, ch NewChannel) bool {
|
||||||
l.Lock()
|
l.Lock()
|
||||||
defer l.Unlock()
|
defer l.Unlock()
|
||||||
for _, f := range l.entries {
|
for _, f := range l.entries {
|
||||||
if laddr.IP.Equal(f.laddr.IP) && laddr.Port == f.laddr.Port {
|
if laddr.Network() == f.laddr.Network() && laddr.String() == f.laddr.String() {
|
||||||
f.c <- forward{ch, &raddr}
|
f.c <- forward{newCh: ch, raddr: raddr}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,7 +295,7 @@ func (l *tcpListener) Accept() (net.Conn, error) {
|
|||||||
}
|
}
|
||||||
go DiscardRequests(incoming)
|
go DiscardRequests(incoming)
|
||||||
|
|
||||||
return &tcpChanConn{
|
return &chanConn{
|
||||||
Channel: ch,
|
Channel: ch,
|
||||||
laddr: l.laddr,
|
laddr: l.laddr,
|
||||||
raddr: s.raddr,
|
raddr: s.raddr,
|
||||||
@@ -277,7 +310,7 @@ func (l *tcpListener) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this also closes the listener.
|
// this also closes the listener.
|
||||||
l.conn.forwards.remove(*l.laddr)
|
l.conn.forwards.remove(l.laddr)
|
||||||
ok, _, err := l.conn.SendRequest("cancel-tcpip-forward", true, Marshal(&m))
|
ok, _, err := l.conn.SendRequest("cancel-tcpip-forward", true, Marshal(&m))
|
||||||
if err == nil && !ok {
|
if err == nil && !ok {
|
||||||
err = errors.New("ssh: cancel-tcpip-forward failed")
|
err = errors.New("ssh: cancel-tcpip-forward failed")
|
||||||
@@ -293,29 +326,52 @@ func (l *tcpListener) Addr() net.Addr {
|
|||||||
// Dial initiates a connection to the addr from the remote host.
|
// Dial initiates a connection to the addr from the remote host.
|
||||||
// The resulting connection has a zero LocalAddr() and RemoteAddr().
|
// The resulting connection has a zero LocalAddr() and RemoteAddr().
|
||||||
func (c *Client) Dial(n, addr string) (net.Conn, error) {
|
func (c *Client) Dial(n, addr string) (net.Conn, error) {
|
||||||
// Parse the address into host and numeric port.
|
var ch Channel
|
||||||
host, portString, err := net.SplitHostPort(addr)
|
switch n {
|
||||||
if err != nil {
|
case "tcp", "tcp4", "tcp6":
|
||||||
return nil, err
|
// Parse the address into host and numeric port.
|
||||||
|
host, portString, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
port, err := strconv.ParseUint(portString, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ch, err = c.dial(net.IPv4zero.String(), 0, host, int(port))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Use a zero address for local and remote address.
|
||||||
|
zeroAddr := &net.TCPAddr{
|
||||||
|
IP: net.IPv4zero,
|
||||||
|
Port: 0,
|
||||||
|
}
|
||||||
|
return &chanConn{
|
||||||
|
Channel: ch,
|
||||||
|
laddr: zeroAddr,
|
||||||
|
raddr: zeroAddr,
|
||||||
|
}, nil
|
||||||
|
case "unix":
|
||||||
|
var err error
|
||||||
|
ch, err = c.dialStreamLocal(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &chanConn{
|
||||||
|
Channel: ch,
|
||||||
|
laddr: &net.UnixAddr{
|
||||||
|
Name: "@",
|
||||||
|
Net: "unix",
|
||||||
|
},
|
||||||
|
raddr: &net.UnixAddr{
|
||||||
|
Name: addr,
|
||||||
|
Net: "unix",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("ssh: unsupported protocol: %s", n)
|
||||||
}
|
}
|
||||||
port, err := strconv.ParseUint(portString, 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Use a zero address for local and remote address.
|
|
||||||
zeroAddr := &net.TCPAddr{
|
|
||||||
IP: net.IPv4zero,
|
|
||||||
Port: 0,
|
|
||||||
}
|
|
||||||
ch, err := c.dial(net.IPv4zero.String(), 0, host, int(port))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &tcpChanConn{
|
|
||||||
Channel: ch,
|
|
||||||
laddr: zeroAddr,
|
|
||||||
raddr: zeroAddr,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialTCP connects to the remote address raddr on the network net,
|
// DialTCP connects to the remote address raddr on the network net,
|
||||||
@@ -332,7 +388,7 @@ func (c *Client) DialTCP(n string, laddr, raddr *net.TCPAddr) (net.Conn, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &tcpChanConn{
|
return &chanConn{
|
||||||
Channel: ch,
|
Channel: ch,
|
||||||
laddr: laddr,
|
laddr: laddr,
|
||||||
raddr: raddr,
|
raddr: raddr,
|
||||||
@@ -366,26 +422,26 @@ type tcpChan struct {
|
|||||||
Channel // the backing channel
|
Channel // the backing channel
|
||||||
}
|
}
|
||||||
|
|
||||||
// tcpChanConn fulfills the net.Conn interface without
|
// chanConn fulfills the net.Conn interface without
|
||||||
// the tcpChan having to hold laddr or raddr directly.
|
// the tcpChan having to hold laddr or raddr directly.
|
||||||
type tcpChanConn struct {
|
type chanConn struct {
|
||||||
Channel
|
Channel
|
||||||
laddr, raddr net.Addr
|
laddr, raddr net.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalAddr returns the local network address.
|
// LocalAddr returns the local network address.
|
||||||
func (t *tcpChanConn) LocalAddr() net.Addr {
|
func (t *chanConn) LocalAddr() net.Addr {
|
||||||
return t.laddr
|
return t.laddr
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoteAddr returns the remote network address.
|
// RemoteAddr returns the remote network address.
|
||||||
func (t *tcpChanConn) RemoteAddr() net.Addr {
|
func (t *chanConn) RemoteAddr() net.Addr {
|
||||||
return t.raddr
|
return t.raddr
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDeadline sets the read and write deadlines associated
|
// SetDeadline sets the read and write deadlines associated
|
||||||
// with the connection.
|
// with the connection.
|
||||||
func (t *tcpChanConn) SetDeadline(deadline time.Time) error {
|
func (t *chanConn) SetDeadline(deadline time.Time) error {
|
||||||
if err := t.SetReadDeadline(deadline); err != nil {
|
if err := t.SetReadDeadline(deadline); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -396,12 +452,14 @@ func (t *tcpChanConn) SetDeadline(deadline time.Time) error {
|
|||||||
// A zero value for t means Read will not time out.
|
// A zero value for t means Read will not time out.
|
||||||
// After the deadline, the error from Read will implement net.Error
|
// After the deadline, the error from Read will implement net.Error
|
||||||
// with Timeout() == true.
|
// with Timeout() == true.
|
||||||
func (t *tcpChanConn) SetReadDeadline(deadline time.Time) error {
|
func (t *chanConn) SetReadDeadline(deadline time.Time) error {
|
||||||
|
// for compatibility with previous version,
|
||||||
|
// the error message contains "tcpChan"
|
||||||
return errors.New("ssh: tcpChan: deadline not supported")
|
return errors.New("ssh: tcpChan: deadline not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWriteDeadline exists to satisfy the net.Conn interface
|
// SetWriteDeadline exists to satisfy the net.Conn interface
|
||||||
// but is not implemented by this type. It always returns an error.
|
// but is not implemented by this type. It always returns an error.
|
||||||
func (t *tcpChanConn) SetWriteDeadline(deadline time.Time) error {
|
func (t *chanConn) SetWriteDeadline(deadline time.Time) error {
|
||||||
return errors.New("ssh: tcpChan: deadline not supported")
|
return errors.New("ssh: tcpChan: deadline not supported")
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+17
-3
@@ -2,6 +2,14 @@
|
|||||||
"comment": "",
|
"comment": "",
|
||||||
"ignore": "test",
|
"ignore": "test",
|
||||||
"package": [
|
"package": [
|
||||||
|
{
|
||||||
|
"checksumSHA1": "EcF7T9tPEMMJfuRdPBB3NdRUg4c=",
|
||||||
|
"path": "github.com/appleboy/easyssh-proxy",
|
||||||
|
"revision": "33d87eae3a018c3312e32cc4eb4578d5a563aabd",
|
||||||
|
"revisionTime": "2017-05-16T07:22:25Z",
|
||||||
|
"version": "1.1.6",
|
||||||
|
"versionExact": "1.1.6"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "dvabztWVQX8f6oMLRyv4dLH+TGY=",
|
"checksumSHA1": "dvabztWVQX8f6oMLRyv4dLH+TGY=",
|
||||||
"path": "github.com/davecgh/go-spew/spew",
|
"path": "github.com/davecgh/go-spew/spew",
|
||||||
@@ -57,10 +65,16 @@
|
|||||||
"revisionTime": "2017-02-08T20:51:15Z"
|
"revisionTime": "2017-02-08T20:51:15Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "fsrFs762jlaILyqqQImS1GfvIvw=",
|
"checksumSHA1": "8sVsMTphul+B0sI0qAv4TE1ZxUk=",
|
||||||
"path": "golang.org/x/crypto/ssh",
|
"path": "golang.org/x/crypto/ssh",
|
||||||
"revision": "453249f01cfeb54c3d549ddb75ff152ca243f9d8",
|
"revision": "cbc3d0884eac986df6e78a039b8792e869bff863",
|
||||||
"revisionTime": "2017-02-08T20:51:15Z"
|
"revisionTime": "2017-04-09T18:29:52Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "SJ3Ma3Ozavxpbh1usZWBCnzMKIc=",
|
||||||
|
"path": "golang.org/x/crypto/ssh/agent",
|
||||||
|
"revision": "40541ccb1c6e64c947ed6f606b8a6cb4b67d7436",
|
||||||
|
"revisionTime": "2017-02-12T21:20:41Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rootPath": "github.com/appleboy/drone-ssh"
|
"rootPath": "github.com/appleboy/drone-ssh"
|
||||||
|
|||||||
Reference in New Issue
Block a user