mirror of
https://github.com/appleboy/drone-ssh.git
synced 2026-06-16 14:49:25 +08:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
+23
-11
@@ -2,17 +2,20 @@ workspace:
|
||||
base: /srv/app
|
||||
path: src/github.com/appleboy/drone-ssh
|
||||
|
||||
pipeline:
|
||||
clone:
|
||||
clone:
|
||||
git:
|
||||
image: plugins/git
|
||||
depth: 50
|
||||
tags: true
|
||||
|
||||
pipeline:
|
||||
test:
|
||||
image: appleboy/golang-testing
|
||||
pull: true
|
||||
environment:
|
||||
TAGS: netgo
|
||||
GOPATH: /srv/app
|
||||
secrets: [ codecov_token ]
|
||||
commands:
|
||||
- make ssh-server
|
||||
- make vet
|
||||
@@ -26,6 +29,16 @@ pipeline:
|
||||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
publish_latest:
|
||||
image: plugins/docker
|
||||
repo: ${DRONE_REPO}
|
||||
tags: [ 'latest' ]
|
||||
secrets: [ docker_username, docker_password ]
|
||||
when:
|
||||
event: [ push ]
|
||||
branch: [ master ]
|
||||
local: false
|
||||
|
||||
release:
|
||||
image: appleboy/golang-testing
|
||||
pull: true
|
||||
@@ -37,27 +50,26 @@ pipeline:
|
||||
when:
|
||||
event: [ tag ]
|
||||
branch: [ refs/tags/* ]
|
||||
local: false
|
||||
|
||||
publish_tag:
|
||||
image: plugins/docker
|
||||
repo: ${DRONE_REPO}
|
||||
tags: [ '${DRONE_TAG}' ]
|
||||
secrets: [ docker_username, docker_password ]
|
||||
group: release
|
||||
when:
|
||||
event: [ tag ]
|
||||
branch: [ refs/tags/* ]
|
||||
local: false
|
||||
|
||||
publish_latest:
|
||||
image: plugins/docker
|
||||
repo: ${DRONE_REPO}
|
||||
tags: [ 'latest' ]
|
||||
when:
|
||||
event: [ push ]
|
||||
branch: [ master ]
|
||||
|
||||
release:
|
||||
release_tag:
|
||||
image: plugins/github-release
|
||||
secrets: [ github_release_api_key ]
|
||||
group: release
|
||||
files:
|
||||
- dist/release/*
|
||||
when:
|
||||
event: [ tag ]
|
||||
branch: [ refs/tags/* ]
|
||||
local: false
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
eyJhbGciOiJIUzI1NiJ9.d29ya3NwYWNlOgogIGJhc2U6IC9zcnYvYXBwCiAgcGF0aDogc3JjL2dpdGh1Yi5jb20vYXBwbGVib3kvZHJvbmUtc3NoCgpwaXBlbGluZToKICBjbG9uZToKICAgIGltYWdlOiBwbHVnaW5zL2dpdAogICAgdGFnczogdHJ1ZQoKICB0ZXN0OgogICAgaW1hZ2U6IGFwcGxlYm95L2dvbGFuZy10ZXN0aW5nCiAgICBwdWxsOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgVEFHUzogbmV0Z28KICAgICAgR09QQVRIOiAvc3J2L2FwcAogICAgY29tbWFuZHM6CiAgICAgIC0gbWFrZSBzc2gtc2VydmVyCiAgICAgIC0gbWFrZSB2ZXQKICAgICAgLSBtYWtlIGxpbnQKICAgICAgIyAtIG1ha2UgdGVzdAogICAgICAtIGNvdmVyYWdlIGFsbAogICAgICAtIG1ha2UgY292ZXJhZ2UKICAgICAgLSBtYWtlIGJ1aWxkCiAgICAgICMgYnVpbGQgYmluYXJ5IGZvciBkb2NrZXIgaW1hZ2UKICAgICAgLSBtYWtlIHN0YXRpY19idWlsZAogICAgd2hlbjoKICAgICAgZXZlbnQ6IFsgcHVzaCwgdGFnLCBwdWxsX3JlcXVlc3QgXQoKICByZWxlYXNlOgogICAgaW1hZ2U6IGFwcGxlYm95L2dvbGFuZy10ZXN0aW5nCiAgICBwdWxsOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgVEFHUzogbmV0Z28KICAgICAgR09QQVRIOiAvc3J2L2FwcAogICAgY29tbWFuZHM6CiAgICAgIC0gbWFrZSByZWxlYXNlCiAgICB3aGVuOgogICAgICBldmVudDogWyB0YWcgXQogICAgICBicmFuY2g6IFsgcmVmcy90YWdzLyogXQoKICBwdWJsaXNoX3RhZzoKICAgIGltYWdlOiBwbHVnaW5zL2RvY2tlcgogICAgcmVwbzogJHtEUk9ORV9SRVBPfQogICAgdGFnczogWyAnJHtEUk9ORV9UQUd9JyBdCiAgICB3aGVuOgogICAgICBldmVudDogWyB0YWcgXQogICAgICBicmFuY2g6IFsgcmVmcy90YWdzLyogXQoKICBwdWJsaXNoX2xhdGVzdDoKICAgIGltYWdlOiBwbHVnaW5zL2RvY2tlcgogICAgcmVwbzogJHtEUk9ORV9SRVBPfQogICAgdGFnczogWyAnbGF0ZXN0JyBdCiAgICB3aGVuOgogICAgICBldmVudDogWyBwdXNoIF0KICAgICAgYnJhbmNoOiBbIG1hc3RlciBdCgogIHJlbGVhc2U6CiAgICBpbWFnZTogcGx1Z2lucy9naXRodWItcmVsZWFzZQogICAgZmlsZXM6CiAgICAgIC0gZGlzdC9yZWxlYXNlLyoKICAgIHdoZW46CiAgICAgIGV2ZW50OiBbIHRhZyBdCiAgICAgIGJyYW5jaDogWyByZWZzL3RhZ3MvKiBdCg.Vz3qqFJL2stx98NSSdyy7d395NG87d-FPedR8LSGPO8
|
||||
@@ -72,6 +72,41 @@ pipeline:
|
||||
- echo world
|
||||
```
|
||||
|
||||
Example configuration for command timeout (unit: second), default value is 60 seconds:
|
||||
|
||||
```diff
|
||||
pipeline:
|
||||
ssh:
|
||||
image: appleboy/drone-ssh
|
||||
host: foo.com
|
||||
username: root
|
||||
password: 1234
|
||||
port: 22
|
||||
+ command_timeout: 10
|
||||
script:
|
||||
- echo hello
|
||||
- echo world
|
||||
```
|
||||
|
||||
Example configuration for execute commands on a remote server using `SSHProxyCommand`:
|
||||
|
||||
```diff
|
||||
pipeline:
|
||||
ssh:
|
||||
image: appleboy/drone-ssh
|
||||
host: foo.com
|
||||
username: root
|
||||
port: 22
|
||||
key: ${DEPLOY_KEY}
|
||||
script:
|
||||
- echo hello
|
||||
- echo world
|
||||
+ proxy_host: 10.130.33.145
|
||||
+ proxy_user: ubuntu
|
||||
+ proxy_port: 22
|
||||
+ proxy_key: ${PROXY_KEY}
|
||||
```
|
||||
|
||||
Example configuration for success build:
|
||||
|
||||
```diff
|
||||
@@ -132,3 +167,24 @@ script
|
||||
|
||||
timeout
|
||||
: 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
|
||||
|
||||
@@ -6,9 +6,11 @@ EXECUTABLE := drone-ssh
|
||||
# for dockerhub
|
||||
DEPLOY_ACCOUNT := appleboy
|
||||
DEPLOY_IMAGE := $(EXECUTABLE)
|
||||
GOFMT ?= gofmt "-s"
|
||||
|
||||
TARGETS ?= linux darwin windows
|
||||
PACKAGES ?= $(shell go list ./... | grep -v /vendor/)
|
||||
GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*")
|
||||
SOURCES ?= $(shell find . -name "*.go" -type f)
|
||||
TAGS ?=
|
||||
LDFLAGS ?= -X 'main.Version=$(VERSION)'
|
||||
@@ -27,8 +29,18 @@ endif
|
||||
|
||||
all: build
|
||||
|
||||
.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;
|
||||
|
||||
fmt:
|
||||
find . -name "*.go" -type f -not -path "./vendor/*" | xargs gofmt -s -w
|
||||
$(GOFMT) -w $(GOFILES)
|
||||
|
||||
vet:
|
||||
go vet $(PACKAGES)
|
||||
@@ -51,7 +63,7 @@ unconvert:
|
||||
fi
|
||||
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;
|
||||
|
||||
html:
|
||||
|
||||
@@ -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.**
|
||||
|
||||

|
||||
## Build
|
||||
|
||||
Build the binary with the following commands:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/appleboy/easyssh-proxy"
|
||||
"github.com/joho/godotenv"
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
"github.com/urfave/cli"
|
||||
@@ -62,6 +63,12 @@ func main() {
|
||||
Usage: "connection 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{
|
||||
Name: "script,s",
|
||||
Usage: "execute commands",
|
||||
@@ -71,6 +78,43 @@ func main() {
|
||||
Name: "env-file",
|
||||
Usage: "source env file",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "proxy.ssh-key",
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
// Override a template
|
||||
@@ -116,14 +160,24 @@ func run(c *cli.Context) error {
|
||||
|
||||
plugin := Plugin{
|
||||
Config: Config{
|
||||
Key: c.String("ssh-key"),
|
||||
KeyPath: c.String("key-path"),
|
||||
UserName: c.String("user"),
|
||||
Password: c.String("password"),
|
||||
Host: c.StringSlice("host"),
|
||||
Port: c.Int("port"),
|
||||
Timeout: c.Duration("timeout"),
|
||||
Script: c.StringSlice("script"),
|
||||
Key: c.String("ssh-key"),
|
||||
KeyPath: c.String("key-path"),
|
||||
UserName: c.String("user"),
|
||||
Password: c.String("password"),
|
||||
Host: c.StringSlice("host"),
|
||||
Port: c.Int("port"),
|
||||
Timeout: c.Duration("timeout"),
|
||||
CommandTimeout: c.Int("command.timeout"),
|
||||
Script: c.StringSlice("script"),
|
||||
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"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -2,33 +2,34 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/appleboy/drone-ssh/easyssh"
|
||||
"github.com/appleboy/easyssh-proxy"
|
||||
)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
const (
|
||||
missingHostOrUser = "Error: missing server host or user"
|
||||
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 (
|
||||
// Config for the plugin.
|
||||
Config struct {
|
||||
Key string
|
||||
KeyPath string
|
||||
UserName string
|
||||
Password string
|
||||
Host []string
|
||||
Port int
|
||||
Timeout time.Duration
|
||||
Script []string
|
||||
Key string
|
||||
KeyPath string
|
||||
UserName string
|
||||
Password string
|
||||
Host []string
|
||||
Port int
|
||||
Timeout time.Duration
|
||||
CommandTimeout int
|
||||
Script []string
|
||||
Proxy easyssh.DefaultConfig
|
||||
}
|
||||
|
||||
// Plugin structure
|
||||
@@ -38,19 +39,28 @@ type (
|
||||
)
|
||||
|
||||
func (p Plugin) log(host string, message ...interface{}) {
|
||||
log.Printf("%s: %s", host, fmt.Sprintln(message...))
|
||||
if count := len(p.Config.Host); count == 1 {
|
||||
fmt.Printf("%s", fmt.Sprintln(message...))
|
||||
} else {
|
||||
fmt.Printf("%s: %s", host, fmt.Sprintln(message...))
|
||||
}
|
||||
}
|
||||
|
||||
// Exec executes the plugin.
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if len(p.Config.Key) != 0 && len(p.Config.Password) != 0 {
|
||||
return fmt.Errorf(setPasswordandKey)
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(p.Config.Host))
|
||||
errChannel := make(chan error, 1)
|
||||
finished := make(chan bool, 1)
|
||||
@@ -65,14 +75,46 @@ func (p Plugin) Exec() error {
|
||||
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, "commands: ", strings.Join(p.Config.Script, "\n"))
|
||||
response, err := ssh.Run(strings.Join(p.Config.Script, "\n"))
|
||||
p.log(host, "outputs:", response)
|
||||
|
||||
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()
|
||||
@@ -88,12 +130,14 @@ func (p Plugin) Exec() error {
|
||||
case <-finished:
|
||||
case err := <-errChannel:
|
||||
if err != nil {
|
||||
log.Println("drone-ssh error: ", err)
|
||||
fmt.Println("drone-ssh error: ", 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
|
||||
}
|
||||
|
||||
+142
-13
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/appleboy/easyssh-proxy"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -29,14 +30,31 @@ func TestMissingKeyOrPassword(t *testing.T) {
|
||||
assert.Equal(t, missingPasswordOrKey, err.Error())
|
||||
}
|
||||
|
||||
func TestSetPasswordAndKey(t *testing.T) {
|
||||
plugin := Plugin{
|
||||
Config{
|
||||
Host: []string{"localhost"},
|
||||
UserName: "ubuntu",
|
||||
Password: "1234",
|
||||
Key: "1234",
|
||||
},
|
||||
}
|
||||
|
||||
err := plugin.Exec()
|
||||
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, setPasswordandKey, err.Error())
|
||||
}
|
||||
|
||||
func TestIncorrectPassword(t *testing.T) {
|
||||
plugin := Plugin{
|
||||
Config: Config{
|
||||
Host: []string{"localhost"},
|
||||
UserName: "drone-scp",
|
||||
Port: 22,
|
||||
Password: "123456",
|
||||
Script: []string{"whoami"},
|
||||
Host: []string{"localhost"},
|
||||
UserName: "drone-scp",
|
||||
Port: 22,
|
||||
Password: "123456",
|
||||
Script: []string{"whoami"},
|
||||
CommandTimeout: 60,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -47,9 +65,10 @@ func TestIncorrectPassword(t *testing.T) {
|
||||
func TestSSHScriptFromRawKey(t *testing.T) {
|
||||
plugin := Plugin{
|
||||
Config: Config{
|
||||
Host: []string{"localhost"},
|
||||
UserName: "drone-scp",
|
||||
Port: 22,
|
||||
Host: []string{"localhost"},
|
||||
UserName: "drone-scp",
|
||||
Port: 22,
|
||||
CommandTimeout: 60,
|
||||
Key: `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA4e2D/qPN08pzTac+a8ZmlP1ziJOXk45CynMPtva0rtK/RB26
|
||||
VbfAF0hIJji7ltvnYnqCU9oFfvEM33cTn7T96+od8ib/Vz25YU8ZbstqtIskPuwC
|
||||
@@ -89,14 +108,124 @@ ib4KbP5ovZlrjL++akMQ7V2fHzuQIFWnCkDA5c2ZAqzlM+ZN+HRG7gWur7Bt4XH1
|
||||
func TestSSHScriptFromKeyFile(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", "ls -al"},
|
||||
Host: []string{"localhost", "127.0.0.1"},
|
||||
UserName: "drone-scp",
|
||||
Port: 22,
|
||||
KeyPath: "./tests/.ssh/id_rsa",
|
||||
Script: []string{"whoami", "ls -al"},
|
||||
CommandTimeout: 60,
|
||||
},
|
||||
}
|
||||
|
||||
err := plugin.Exec()
|
||||
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)
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.9 MiB |
+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
|
||||
// public key that is not a certificate. It must implement host key
|
||||
// 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
|
||||
// can be implemented. It should return true if the given certificate
|
||||
|
||||
+51
-5
@@ -5,6 +5,7 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -13,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
// 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 {
|
||||
Conn
|
||||
|
||||
@@ -59,6 +60,7 @@ func NewClient(c Conn, chans <-chan NewChannel, reqs <-chan *Request) *Client {
|
||||
conn.forwards.closeAll()
|
||||
}()
|
||||
go conn.forwards.handleChannels(conn.HandleChannelOpen("forwarded-tcpip"))
|
||||
go conn.forwards.handleChannels(conn.HandleChannelOpen("forwarded-streamlocal@openssh.com"))
|
||||
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) {
|
||||
fullConf := *config
|
||||
fullConf.SetDefaults()
|
||||
if fullConf.HostKeyCallback == nil {
|
||||
c.Close()
|
||||
return nil, nil, nil, errors.New("ssh: must specify HostKeyCallback")
|
||||
}
|
||||
|
||||
conn := &connection{
|
||||
sshConn: sshConn{conn: c},
|
||||
}
|
||||
@@ -173,6 +180,13 @@ func Dial(network, addr string, config *ClientConfig) (*Client, error) {
|
||||
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
|
||||
// modified after having been passed to an SSH function.
|
||||
type ClientConfig struct {
|
||||
@@ -188,10 +202,12 @@ type ClientConfig struct {
|
||||
// be used during authentication.
|
||||
Auth []AuthMethod
|
||||
|
||||
// HostKeyCallback, if not nil, is called during the cryptographic
|
||||
// handshake to validate the server's host key. A nil HostKeyCallback
|
||||
// implies that all host keys are accepted.
|
||||
HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
|
||||
// HostKeyCallback is called during the cryptographic
|
||||
// handshake to validate the server's host key. The client
|
||||
// configuration must supply this callback for the connection
|
||||
// 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
|
||||
// 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.
|
||||
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) {
|
||||
// Authentication is performed in two stages. The first stage sends an
|
||||
// enquiry to test if each key is acceptable to the remote. The second
|
||||
// stage attempts to authenticate with the valid keys obtained in the
|
||||
// first stage.
|
||||
// Authentication is performed by sending an enquiry to test if a key is
|
||||
// acceptable to the remote. If the key is acceptable, the client will
|
||||
// attempt to authenticate with the valid key. If not the client will repeat
|
||||
// the process with the remaining keys.
|
||||
|
||||
signers, err := cb()
|
||||
if err != nil {
|
||||
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
|
||||
for _, signer := range validKeys {
|
||||
pub := signer.PublicKey()
|
||||
for _, signer := range signers {
|
||||
ok, err := validateKey(signer.PublicKey(), user, c)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
pub := signer.PublicKey()
|
||||
pubKey := pub.Marshal()
|
||||
sign, err := signer.Sign(rand, buildDataSignedForAuth(session, userAuthRequestMsg{
|
||||
User: user,
|
||||
@@ -236,13 +231,29 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
|
||||
if err != nil {
|
||||
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 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.
|
||||
func validateKey(key PublicKey, user string, c packetConn) (bool, error) {
|
||||
pubKey := key.Marshal()
|
||||
|
||||
+8
-6
@@ -9,6 +9,7 @@ import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
_ "crypto/sha1"
|
||||
@@ -40,7 +41,7 @@ var supportedKexAlgos = []string{
|
||||
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.
|
||||
var supportedHostKeyAlgos = []string{
|
||||
CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01,
|
||||
@@ -186,7 +187,7 @@ type Config struct {
|
||||
|
||||
// The maximum number of bytes sent or received after which a
|
||||
// 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
|
||||
|
||||
// The allowed key exchanges algorithms. If unspecified then a
|
||||
@@ -230,11 +231,12 @@ func (c *Config) SetDefaults() {
|
||||
}
|
||||
|
||||
if c.RekeyThreshold == 0 {
|
||||
// RFC 4253, section 9 suggests rekeying after 1G.
|
||||
c.RekeyThreshold = 1 << 30
|
||||
}
|
||||
if c.RekeyThreshold < minRekeyThreshold {
|
||||
// cipher specific default
|
||||
} else if 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:
|
||||
[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
|
||||
|
||||
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"
|
||||
|
||||
+34
-19
@@ -74,7 +74,7 @@ type handshakeTransport struct {
|
||||
startKex chan *pendingKex
|
||||
|
||||
// data for host key checking
|
||||
hostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
|
||||
hostKeyCallback HostKeyCallback
|
||||
dialAddress string
|
||||
remoteAddr net.Addr
|
||||
|
||||
@@ -107,6 +107,8 @@ func newHandshakeTransport(conn keyingTransport, config *Config, clientVersion,
|
||||
|
||||
config: config,
|
||||
}
|
||||
t.resetReadThresholds()
|
||||
t.resetWriteThresholds()
|
||||
|
||||
// We always start with a mandatory key exchange.
|
||||
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() {
|
||||
|
||||
write:
|
||||
@@ -285,12 +298,8 @@ write:
|
||||
t.writeError = err
|
||||
t.sentInitPacket = nil
|
||||
t.sentInitMsg = nil
|
||||
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()
|
||||
}
|
||||
|
||||
t.resetWriteThresholds()
|
||||
|
||||
// we have completed the key exchange. Since the
|
||||
// reader is still blocked, it is safe to clear out
|
||||
@@ -344,6 +353,17 @@ write:
|
||||
// key exchange itself.
|
||||
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) {
|
||||
p, err := t.conn.readPacket()
|
||||
if err != nil {
|
||||
@@ -391,12 +411,7 @@ func (t *handshakeTransport) readOnePacket(first bool) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t.readPacketsLeft = packetRekeyThreshold
|
||||
if t.config.RekeyThreshold > 0 {
|
||||
t.readBytesLeft = int64(t.config.RekeyThreshold)
|
||||
} else {
|
||||
t.readBytesLeft = t.algorithms.r.rekeyBytes()
|
||||
}
|
||||
t.resetReadThresholds()
|
||||
|
||||
// By default, a key exchange is hidden from higher layers by
|
||||
// translating it into msgIgnore.
|
||||
@@ -574,7 +589,9 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error {
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -614,11 +631,9 @@ func (t *handshakeTransport) client(kex kexAlgorithm, algs *algorithms, magics *
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if t.hostKeyCallback != nil {
|
||||
err = t.hostKeyCallback(t.dialAddress, t.remoteAddr, hostKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = t.hostKeyCallback(t.dialAddress, t.remoteAddr, hostKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
+73
-21
@@ -824,7 +824,7 @@ func ParseDSAPrivateKey(der []byte) (*dsa.PrivateKey, error) {
|
||||
|
||||
// Implemented based on the documentation at
|
||||
// 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)
|
||||
if !bytes.Equal(magic, key[0:len(magic)]) {
|
||||
return nil, errors.New("ssh: invalid openssh private key format")
|
||||
@@ -844,14 +844,15 @@ func parseOpenSSHPrivateKey(key []byte) (*ed25519.PrivateKey, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if w.KdfName != "none" || w.CipherName != "none" {
|
||||
return nil, errors.New("ssh: cannot decode encrypted private keys")
|
||||
}
|
||||
|
||||
pk1 := struct {
|
||||
Check1 uint32
|
||||
Check2 uint32
|
||||
Keytype string
|
||||
Pub []byte
|
||||
Priv []byte
|
||||
Comment string
|
||||
Pad []byte `ssh:"rest"`
|
||||
Rest []byte `ssh:"rest"`
|
||||
}{}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// we only handle ed25519 keys currently
|
||||
if pk1.Keytype != KeyAlgoED25519 {
|
||||
// we only handle ed25519 and rsa keys currently
|
||||
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")
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
+33
@@ -45,6 +45,12 @@ type ServerConfig struct {
|
||||
// authenticating.
|
||||
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
|
||||
// attempts to authenticate using a password.
|
||||
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
|
||||
// will hang.
|
||||
func NewServerConn(c net.Conn, config *ServerConfig) (*ServerConn, <-chan NewChannel, <-chan *Request, error) {
|
||||
if config.MaxAuthTries == 0 {
|
||||
config.MaxAuthTries = 6
|
||||
}
|
||||
|
||||
fullConf := *config
|
||||
fullConf.SetDefaults()
|
||||
s := &connection{
|
||||
@@ -267,8 +277,23 @@ func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, err
|
||||
var cache pubKeyCache
|
||||
var perms *Permissions
|
||||
|
||||
authFailures := 0
|
||||
|
||||
userAuthLoop:
|
||||
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
|
||||
if packet, err := s.transport.readPacket(); err != nil {
|
||||
return nil, err
|
||||
@@ -289,6 +314,11 @@ userAuthLoop:
|
||||
if config.NoClientAuth {
|
||||
authErr = nil
|
||||
}
|
||||
|
||||
// allow initial attempt of 'none' without penalty
|
||||
if authFailures == 0 {
|
||||
authFailures--
|
||||
}
|
||||
case "password":
|
||||
if config.PasswordCallback == nil {
|
||||
authErr = errors.New("ssh: password auth not configured")
|
||||
@@ -360,6 +390,7 @@ userAuthLoop:
|
||||
if isQuery {
|
||||
// The client can query if the given public key
|
||||
// would be okay.
|
||||
|
||||
if len(payload) > 0 {
|
||||
return nil, parseError(msgUserAuthRequest)
|
||||
}
|
||||
@@ -409,6 +440,8 @@ userAuthLoop:
|
||||
break userAuthLoop
|
||||
}
|
||||
|
||||
authFailures++
|
||||
|
||||
var failureMsg userAuthFailureMsg
|
||||
if config.PasswordCallback != nil {
|
||||
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
|
||||
// the returned net.Listener. The listener must be serviced, or the
|
||||
// SSH connection may hang.
|
||||
// N must be "tcp", "tcp4", "tcp6", or "unix".
|
||||
func (c *Client) Listen(n, addr string) (net.Listener, error) {
|
||||
laddr, err := net.ResolveTCPAddr(n, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
switch n {
|
||||
case "tcp", "tcp4", "tcp6":
|
||||
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
|
||||
@@ -116,7 +124,7 @@ func (c *Client) ListenTCP(laddr *net.TCPAddr) (net.Listener, error) {
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -131,7 +139,7 @@ type forwardList struct {
|
||||
// forwardEntry represents an established mapping of a laddr on a
|
||||
// remote ssh server to a channel connected to a tcpListener.
|
||||
type forwardEntry struct {
|
||||
laddr net.TCPAddr
|
||||
laddr net.Addr
|
||||
c chan forward
|
||||
}
|
||||
|
||||
@@ -139,16 +147,16 @@ type forwardEntry struct {
|
||||
// arguments to add/remove/lookup should be address as specified in
|
||||
// the original forward-request.
|
||||
type forward struct {
|
||||
newCh NewChannel // the ssh client channel underlying this forward
|
||||
raddr *net.TCPAddr // the raddr of the incoming connection
|
||||
newCh NewChannel // the ssh client channel underlying this forward
|
||||
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()
|
||||
defer l.Unlock()
|
||||
f := forwardEntry{
|
||||
addr,
|
||||
make(chan forward, 1),
|
||||
laddr: addr,
|
||||
c: make(chan forward, 1),
|
||||
}
|
||||
l.entries = append(l.entries, f)
|
||||
return f.c
|
||||
@@ -176,44 +184,69 @@ func parseTCPAddr(addr string, port uint32) (*net.TCPAddr, error) {
|
||||
|
||||
func (l *forwardList) handleChannels(in <-chan NewChannel) {
|
||||
for ch := range in {
|
||||
var payload forwardedTCPPayload
|
||||
if err := Unmarshal(ch.ExtraData(), &payload); err != nil {
|
||||
ch.Reject(ConnectionFailed, "could not parse forwarded-tcpip payload: "+err.Error())
|
||||
continue
|
||||
}
|
||||
var (
|
||||
laddr net.Addr
|
||||
raddr net.Addr
|
||||
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
|
||||
// addresses should list the address, in string
|
||||
// format. It is implied that this should be an IP
|
||||
// address, as it would be impossible to connect to it
|
||||
// otherwise.
|
||||
laddr, err := parseTCPAddr(payload.Addr, payload.Port)
|
||||
if err != nil {
|
||||
ch.Reject(ConnectionFailed, err.Error())
|
||||
continue
|
||||
}
|
||||
raddr, err := parseTCPAddr(payload.OriginAddr, payload.OriginPort)
|
||||
if err != nil {
|
||||
ch.Reject(ConnectionFailed, err.Error())
|
||||
continue
|
||||
}
|
||||
// RFC 4254 section 7.2 specifies that incoming
|
||||
// addresses should list the address, in string
|
||||
// format. It is implied that this should be an IP
|
||||
// address, as it would be impossible to connect to it
|
||||
// otherwise.
|
||||
laddr, err = parseTCPAddr(payload.Addr, payload.Port)
|
||||
if err != nil {
|
||||
ch.Reject(ConnectionFailed, err.Error())
|
||||
continue
|
||||
}
|
||||
raddr, err = parseTCPAddr(payload.OriginAddr, payload.OriginPort)
|
||||
if err != nil {
|
||||
ch.Reject(ConnectionFailed, err.Error())
|
||||
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
|
||||
// connections.
|
||||
ch.Reject(Prohibited, "no forward for address")
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// remove removes the forward entry, and the channel feeding its
|
||||
// listener.
|
||||
func (l *forwardList) remove(addr net.TCPAddr) {
|
||||
func (l *forwardList) remove(addr net.Addr) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
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:]...)
|
||||
close(f.c)
|
||||
return
|
||||
@@ -231,12 +264,12 @@ func (l *forwardList) closeAll() {
|
||||
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()
|
||||
defer l.Unlock()
|
||||
for _, f := range l.entries {
|
||||
if laddr.IP.Equal(f.laddr.IP) && laddr.Port == f.laddr.Port {
|
||||
f.c <- forward{ch, &raddr}
|
||||
if laddr.Network() == f.laddr.Network() && laddr.String() == f.laddr.String() {
|
||||
f.c <- forward{newCh: ch, raddr: raddr}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -262,7 +295,7 @@ func (l *tcpListener) Accept() (net.Conn, error) {
|
||||
}
|
||||
go DiscardRequests(incoming)
|
||||
|
||||
return &tcpChanConn{
|
||||
return &chanConn{
|
||||
Channel: ch,
|
||||
laddr: l.laddr,
|
||||
raddr: s.raddr,
|
||||
@@ -277,7 +310,7 @@ func (l *tcpListener) Close() error {
|
||||
}
|
||||
|
||||
// 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))
|
||||
if err == nil && !ok {
|
||||
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.
|
||||
// The resulting connection has a zero LocalAddr() and RemoteAddr().
|
||||
func (c *Client) Dial(n, addr string) (net.Conn, error) {
|
||||
// Parse the address into host and numeric port.
|
||||
host, portString, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var ch Channel
|
||||
switch n {
|
||||
case "tcp", "tcp4", "tcp6":
|
||||
// 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,
|
||||
@@ -332,7 +388,7 @@ func (c *Client) DialTCP(n string, laddr, raddr *net.TCPAddr) (net.Conn, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tcpChanConn{
|
||||
return &chanConn{
|
||||
Channel: ch,
|
||||
laddr: laddr,
|
||||
raddr: raddr,
|
||||
@@ -366,26 +422,26 @@ type tcpChan struct {
|
||||
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.
|
||||
type tcpChanConn struct {
|
||||
type chanConn struct {
|
||||
Channel
|
||||
laddr, raddr net.Addr
|
||||
}
|
||||
|
||||
// LocalAddr returns the local network address.
|
||||
func (t *tcpChanConn) LocalAddr() net.Addr {
|
||||
func (t *chanConn) LocalAddr() net.Addr {
|
||||
return t.laddr
|
||||
}
|
||||
|
||||
// RemoteAddr returns the remote network address.
|
||||
func (t *tcpChanConn) RemoteAddr() net.Addr {
|
||||
func (t *chanConn) RemoteAddr() net.Addr {
|
||||
return t.raddr
|
||||
}
|
||||
|
||||
// SetDeadline sets the read and write deadlines associated
|
||||
// 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 {
|
||||
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.
|
||||
// After the deadline, the error from Read will implement net.Error
|
||||
// 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")
|
||||
}
|
||||
|
||||
// SetWriteDeadline exists to satisfy the net.Conn interface
|
||||
// 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")
|
||||
}
|
||||
|
||||
Vendored
+17
-3
@@ -2,6 +2,14 @@
|
||||
"comment": "",
|
||||
"ignore": "test",
|
||||
"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=",
|
||||
"path": "github.com/davecgh/go-spew/spew",
|
||||
@@ -57,10 +65,16 @@
|
||||
"revisionTime": "2017-02-08T20:51:15Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "fsrFs762jlaILyqqQImS1GfvIvw=",
|
||||
"checksumSHA1": "8sVsMTphul+B0sI0qAv4TE1ZxUk=",
|
||||
"path": "golang.org/x/crypto/ssh",
|
||||
"revision": "453249f01cfeb54c3d549ddb75ff152ca243f9d8",
|
||||
"revisionTime": "2017-02-08T20:51:15Z"
|
||||
"revision": "cbc3d0884eac986df6e78a039b8792e869bff863",
|
||||
"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"
|
||||
|
||||
Reference in New Issue
Block a user