go.crypto: initial code
Manual edits to README. Moved from main Go repository, deleted Makefiles, ran gofix -r go1rename. Tested with: go test code.google.com/p/go.crypto/... R=golang-dev, bradfitz CC=golang-dev https://golang.org/cl/5564059
This commit is contained in:
+318
@@ -0,0 +1,318 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A Channel is an ordered, reliable, duplex stream that is multiplexed over an
|
||||
// SSH connection.
|
||||
type Channel interface {
|
||||
// Accept accepts the channel creation request.
|
||||
Accept() error
|
||||
// Reject rejects the channel creation request. After calling this, no
|
||||
// other methods on the Channel may be called. If they are then the
|
||||
// peer is likely to signal a protocol error and drop the connection.
|
||||
Reject(reason RejectionReason, message string) error
|
||||
|
||||
// Read may return a ChannelRequest as an error.
|
||||
Read(data []byte) (int, error)
|
||||
Write(data []byte) (int, error)
|
||||
Close() error
|
||||
|
||||
// AckRequest either sends an ack or nack to the channel request.
|
||||
AckRequest(ok bool) error
|
||||
|
||||
// ChannelType returns the type of the channel, as supplied by the
|
||||
// client.
|
||||
ChannelType() string
|
||||
// ExtraData returns the arbitary payload for this channel, as supplied
|
||||
// by the client. This data is specific to the channel type.
|
||||
ExtraData() []byte
|
||||
}
|
||||
|
||||
// ChannelRequest represents a request sent on a channel, outside of the normal
|
||||
// stream of bytes. It may result from calling Read on a Channel.
|
||||
type ChannelRequest struct {
|
||||
Request string
|
||||
WantReply bool
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func (c ChannelRequest) Error() string {
|
||||
return "channel request received"
|
||||
}
|
||||
|
||||
// RejectionReason is an enumeration used when rejecting channel creation
|
||||
// requests. See RFC 4254, section 5.1.
|
||||
type RejectionReason int
|
||||
|
||||
const (
|
||||
Prohibited RejectionReason = iota + 1
|
||||
ConnectionFailed
|
||||
UnknownChannelType
|
||||
ResourceShortage
|
||||
)
|
||||
|
||||
type channel struct {
|
||||
// immutable once created
|
||||
chanType string
|
||||
extraData []byte
|
||||
|
||||
theyClosed bool
|
||||
theySentEOF bool
|
||||
weClosed bool
|
||||
dead bool
|
||||
|
||||
serverConn *ServerConn
|
||||
myId, theirId uint32
|
||||
myWindow, theirWindow uint32
|
||||
maxPacketSize uint32
|
||||
err error
|
||||
|
||||
pendingRequests []ChannelRequest
|
||||
pendingData []byte
|
||||
head, length int
|
||||
|
||||
// This lock is inferior to serverConn.lock
|
||||
lock sync.Mutex
|
||||
cond *sync.Cond
|
||||
}
|
||||
|
||||
func (c *channel) Accept() error {
|
||||
c.serverConn.lock.Lock()
|
||||
defer c.serverConn.lock.Unlock()
|
||||
|
||||
if c.serverConn.err != nil {
|
||||
return c.serverConn.err
|
||||
}
|
||||
|
||||
confirm := channelOpenConfirmMsg{
|
||||
PeersId: c.theirId,
|
||||
MyId: c.myId,
|
||||
MyWindow: c.myWindow,
|
||||
MaxPacketSize: c.maxPacketSize,
|
||||
}
|
||||
return c.serverConn.writePacket(marshal(msgChannelOpenConfirm, confirm))
|
||||
}
|
||||
|
||||
func (c *channel) Reject(reason RejectionReason, message string) error {
|
||||
c.serverConn.lock.Lock()
|
||||
defer c.serverConn.lock.Unlock()
|
||||
|
||||
if c.serverConn.err != nil {
|
||||
return c.serverConn.err
|
||||
}
|
||||
|
||||
reject := channelOpenFailureMsg{
|
||||
PeersId: c.theirId,
|
||||
Reason: uint32(reason),
|
||||
Message: message,
|
||||
Language: "en",
|
||||
}
|
||||
return c.serverConn.writePacket(marshal(msgChannelOpenFailure, reject))
|
||||
}
|
||||
|
||||
func (c *channel) handlePacket(packet interface{}) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
switch packet := packet.(type) {
|
||||
case *channelRequestMsg:
|
||||
req := ChannelRequest{
|
||||
Request: packet.Request,
|
||||
WantReply: packet.WantReply,
|
||||
Payload: packet.RequestSpecificData,
|
||||
}
|
||||
|
||||
c.pendingRequests = append(c.pendingRequests, req)
|
||||
c.cond.Signal()
|
||||
case *channelCloseMsg:
|
||||
c.theyClosed = true
|
||||
c.cond.Signal()
|
||||
case *channelEOFMsg:
|
||||
c.theySentEOF = true
|
||||
c.cond.Signal()
|
||||
default:
|
||||
panic("unknown packet type")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *channel) handleData(data []byte) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
// The other side should never send us more than our window.
|
||||
if len(data)+c.length > len(c.pendingData) {
|
||||
// TODO(agl): we should tear down the channel with a protocol
|
||||
// error.
|
||||
return
|
||||
}
|
||||
|
||||
c.myWindow -= uint32(len(data))
|
||||
for i := 0; i < 2; i++ {
|
||||
tail := c.head + c.length
|
||||
if tail > len(c.pendingData) {
|
||||
tail -= len(c.pendingData)
|
||||
}
|
||||
n := copy(c.pendingData[tail:], data)
|
||||
data = data[n:]
|
||||
c.length += n
|
||||
}
|
||||
|
||||
c.cond.Signal()
|
||||
}
|
||||
|
||||
func (c *channel) Read(data []byte) (n int, err error) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if c.err != nil {
|
||||
return 0, c.err
|
||||
}
|
||||
|
||||
if c.myWindow <= uint32(len(c.pendingData))/2 {
|
||||
packet := marshal(msgChannelWindowAdjust, windowAdjustMsg{
|
||||
PeersId: c.theirId,
|
||||
AdditionalBytes: uint32(len(c.pendingData)) - c.myWindow,
|
||||
})
|
||||
if err := c.serverConn.writePacket(packet); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
if c.theySentEOF || c.theyClosed || c.dead {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
if len(c.pendingRequests) > 0 {
|
||||
req := c.pendingRequests[0]
|
||||
if len(c.pendingRequests) == 1 {
|
||||
c.pendingRequests = nil
|
||||
} else {
|
||||
oldPendingRequests := c.pendingRequests
|
||||
c.pendingRequests = make([]ChannelRequest, len(oldPendingRequests)-1)
|
||||
copy(c.pendingRequests, oldPendingRequests[1:])
|
||||
}
|
||||
|
||||
return 0, req
|
||||
}
|
||||
|
||||
if c.length > 0 {
|
||||
tail := c.head + c.length
|
||||
if tail > len(c.pendingData) {
|
||||
tail -= len(c.pendingData)
|
||||
}
|
||||
n = copy(data, c.pendingData[c.head:tail])
|
||||
c.head += n
|
||||
c.length -= n
|
||||
if c.head == len(c.pendingData) {
|
||||
c.head = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.cond.Wait()
|
||||
}
|
||||
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func (c *channel) Write(data []byte) (n int, err error) {
|
||||
for len(data) > 0 {
|
||||
c.lock.Lock()
|
||||
if c.dead || c.weClosed {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
if c.theirWindow == 0 {
|
||||
c.cond.Wait()
|
||||
continue
|
||||
}
|
||||
c.lock.Unlock()
|
||||
|
||||
todo := data
|
||||
if uint32(len(todo)) > c.theirWindow {
|
||||
todo = todo[:c.theirWindow]
|
||||
}
|
||||
|
||||
packet := make([]byte, 1+4+4+len(todo))
|
||||
packet[0] = msgChannelData
|
||||
packet[1] = byte(c.theirId >> 24)
|
||||
packet[2] = byte(c.theirId >> 16)
|
||||
packet[3] = byte(c.theirId >> 8)
|
||||
packet[4] = byte(c.theirId)
|
||||
packet[5] = byte(len(todo) >> 24)
|
||||
packet[6] = byte(len(todo) >> 16)
|
||||
packet[7] = byte(len(todo) >> 8)
|
||||
packet[8] = byte(len(todo))
|
||||
copy(packet[9:], todo)
|
||||
|
||||
c.serverConn.lock.Lock()
|
||||
if err = c.serverConn.writePacket(packet); err != nil {
|
||||
c.serverConn.lock.Unlock()
|
||||
return
|
||||
}
|
||||
c.serverConn.lock.Unlock()
|
||||
|
||||
n += len(todo)
|
||||
data = data[len(todo):]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *channel) Close() error {
|
||||
c.serverConn.lock.Lock()
|
||||
defer c.serverConn.lock.Unlock()
|
||||
|
||||
if c.serverConn.err != nil {
|
||||
return c.serverConn.err
|
||||
}
|
||||
|
||||
if c.weClosed {
|
||||
return errors.New("ssh: channel already closed")
|
||||
}
|
||||
c.weClosed = true
|
||||
|
||||
closeMsg := channelCloseMsg{
|
||||
PeersId: c.theirId,
|
||||
}
|
||||
return c.serverConn.writePacket(marshal(msgChannelClose, closeMsg))
|
||||
}
|
||||
|
||||
func (c *channel) AckRequest(ok bool) error {
|
||||
c.serverConn.lock.Lock()
|
||||
defer c.serverConn.lock.Unlock()
|
||||
|
||||
if c.serverConn.err != nil {
|
||||
return c.serverConn.err
|
||||
}
|
||||
|
||||
if ok {
|
||||
ack := channelRequestSuccessMsg{
|
||||
PeersId: c.theirId,
|
||||
}
|
||||
return c.serverConn.writePacket(marshal(msgChannelSuccess, ack))
|
||||
} else {
|
||||
ack := channelRequestFailureMsg{
|
||||
PeersId: c.theirId,
|
||||
}
|
||||
return c.serverConn.writePacket(marshal(msgChannelFailure, ack))
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func (c *channel) ChannelType() string {
|
||||
return c.chanType
|
||||
}
|
||||
|
||||
func (c *channel) ExtraData() []byte {
|
||||
return c.extraData
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rc4"
|
||||
)
|
||||
|
||||
// streamDump is used to dump the initial keystream for stream ciphers. It is a
|
||||
// a write-only buffer, and not intended for reading so do not require a mutex.
|
||||
var streamDump [512]byte
|
||||
|
||||
// noneCipher implements cipher.Stream and provides no encryption. It is used
|
||||
// by the transport before the first key-exchange.
|
||||
type noneCipher struct{}
|
||||
|
||||
func (c noneCipher) XORKeyStream(dst, src []byte) {
|
||||
copy(dst, src)
|
||||
}
|
||||
|
||||
func newAESCTR(key, iv []byte) (cipher.Stream, error) {
|
||||
c, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cipher.NewCTR(c, iv), nil
|
||||
}
|
||||
|
||||
func newRC4(key, iv []byte) (cipher.Stream, error) {
|
||||
return rc4.NewCipher(key)
|
||||
}
|
||||
|
||||
type cipherMode struct {
|
||||
keySize int
|
||||
ivSize int
|
||||
skip int
|
||||
createFn func(key, iv []byte) (cipher.Stream, error)
|
||||
}
|
||||
|
||||
func (c *cipherMode) createCipher(key, iv []byte) (cipher.Stream, error) {
|
||||
if len(key) < c.keySize {
|
||||
panic("ssh: key length too small for cipher")
|
||||
}
|
||||
if len(iv) < c.ivSize {
|
||||
panic("ssh: iv too small for cipher")
|
||||
}
|
||||
|
||||
stream, err := c.createFn(key[:c.keySize], iv[:c.ivSize])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for remainingToDump := c.skip; remainingToDump > 0; {
|
||||
dumpThisTime := remainingToDump
|
||||
if dumpThisTime > len(streamDump) {
|
||||
dumpThisTime = len(streamDump)
|
||||
}
|
||||
stream.XORKeyStream(streamDump[:dumpThisTime], streamDump[:dumpThisTime])
|
||||
remainingToDump -= dumpThisTime
|
||||
}
|
||||
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
// Specifies a default set of ciphers and a preference order. This is based on
|
||||
// OpenSSH's default client preference order, minus algorithms that are not
|
||||
// implemented.
|
||||
var DefaultCipherOrder = []string{
|
||||
"aes128-ctr", "aes192-ctr", "aes256-ctr",
|
||||
"arcfour256", "arcfour128",
|
||||
}
|
||||
|
||||
var cipherModes = map[string]*cipherMode{
|
||||
// Ciphers from RFC4344, which introduced many CTR-based ciphers. Algorithms
|
||||
// are defined in the order specified in the RFC.
|
||||
"aes128-ctr": {16, aes.BlockSize, 0, newAESCTR},
|
||||
"aes192-ctr": {24, aes.BlockSize, 0, newAESCTR},
|
||||
"aes256-ctr": {32, aes.BlockSize, 0, newAESCTR},
|
||||
|
||||
// Ciphers from RFC4345, which introduces security-improved arcfour ciphers.
|
||||
// They are defined in the order specified in the RFC.
|
||||
"arcfour128": {16, 0, 1536, newRC4},
|
||||
"arcfour256": {32, 0, 1536, newRC4},
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCipherReversal tests that each cipher factory produces ciphers that can
|
||||
// encrypt and decrypt some data successfully.
|
||||
func TestCipherReversal(t *testing.T) {
|
||||
testData := []byte("abcdefghijklmnopqrstuvwxyz012345")
|
||||
testKey := []byte("AbCdEfGhIjKlMnOpQrStUvWxYz012345")
|
||||
testIv := []byte("sdflkjhsadflkjhasdflkjhsadfklhsa")
|
||||
|
||||
cryptBuffer := make([]byte, 32)
|
||||
|
||||
for name, cipherMode := range cipherModes {
|
||||
encrypter, err := cipherMode.createCipher(testKey, testIv)
|
||||
if err != nil {
|
||||
t.Errorf("failed to create encrypter for %q: %s", name, err)
|
||||
continue
|
||||
}
|
||||
decrypter, err := cipherMode.createCipher(testKey, testIv)
|
||||
if err != nil {
|
||||
t.Errorf("failed to create decrypter for %q: %s", name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
copy(cryptBuffer, testData)
|
||||
|
||||
encrypter.XORKeyStream(cryptBuffer, cryptBuffer)
|
||||
if name == "none" {
|
||||
if !bytes.Equal(cryptBuffer, testData) {
|
||||
t.Errorf("encryption made change with 'none' cipher")
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if bytes.Equal(cryptBuffer, testData) {
|
||||
t.Errorf("encryption made no change with %q", name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
decrypter.XORKeyStream(cryptBuffer, cryptBuffer)
|
||||
if !bytes.Equal(cryptBuffer, testData) {
|
||||
t.Errorf("decrypted bytes not equal to input with %q", name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultCiphersExist(t *testing.T) {
|
||||
for _, cipherAlgo := range DefaultCipherOrder {
|
||||
if _, ok := cipherModes[cipherAlgo]; !ok {
|
||||
t.Errorf("default cipher %q is unknown", cipherAlgo)
|
||||
}
|
||||
}
|
||||
}
|
||||
+505
@@ -0,0 +1,505 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// clientVersion is the fixed identification string that the client will use.
|
||||
var clientVersion = []byte("SSH-2.0-Go\r\n")
|
||||
|
||||
// ClientConn represents the client side of an SSH connection.
|
||||
type ClientConn struct {
|
||||
*transport
|
||||
config *ClientConfig
|
||||
chanlist
|
||||
}
|
||||
|
||||
// Client returns a new SSH client connection using c as the underlying transport.
|
||||
func Client(c net.Conn, config *ClientConfig) (*ClientConn, error) {
|
||||
conn := &ClientConn{
|
||||
transport: newTransport(c, config.rand()),
|
||||
config: config,
|
||||
}
|
||||
if err := conn.handshake(); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
go conn.mainLoop()
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// handshake performs the client side key exchange. See RFC 4253 Section 7.
|
||||
func (c *ClientConn) handshake() error {
|
||||
var magics handshakeMagics
|
||||
|
||||
if _, err := c.Write(clientVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
magics.clientVersion = clientVersion[:len(clientVersion)-2]
|
||||
|
||||
// read remote server version
|
||||
version, err := readVersion(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
magics.serverVersion = version
|
||||
clientKexInit := kexInitMsg{
|
||||
KexAlgos: supportedKexAlgos,
|
||||
ServerHostKeyAlgos: supportedHostKeyAlgos,
|
||||
CiphersClientServer: c.config.Crypto.ciphers(),
|
||||
CiphersServerClient: c.config.Crypto.ciphers(),
|
||||
MACsClientServer: supportedMACs,
|
||||
MACsServerClient: supportedMACs,
|
||||
CompressionClientServer: supportedCompressions,
|
||||
CompressionServerClient: supportedCompressions,
|
||||
}
|
||||
kexInitPacket := marshal(msgKexInit, clientKexInit)
|
||||
magics.clientKexInit = kexInitPacket
|
||||
|
||||
if err := c.writePacket(kexInitPacket); err != nil {
|
||||
return err
|
||||
}
|
||||
packet, err := c.readPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
magics.serverKexInit = packet
|
||||
|
||||
var serverKexInit kexInitMsg
|
||||
if err = unmarshal(&serverKexInit, packet, msgKexInit); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kexAlgo, hostKeyAlgo, ok := findAgreedAlgorithms(c.transport, &clientKexInit, &serverKexInit)
|
||||
if !ok {
|
||||
return errors.New("ssh: no common algorithms")
|
||||
}
|
||||
|
||||
if serverKexInit.FirstKexFollows && kexAlgo != serverKexInit.KexAlgos[0] {
|
||||
// The server sent a Kex message for the wrong algorithm,
|
||||
// which we have to ignore.
|
||||
if _, err := c.readPacket(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var H, K []byte
|
||||
var hashFunc crypto.Hash
|
||||
switch kexAlgo {
|
||||
case kexAlgoDH14SHA1:
|
||||
hashFunc = crypto.SHA1
|
||||
dhGroup14Once.Do(initDHGroup14)
|
||||
H, K, err = c.kexDH(dhGroup14, hashFunc, &magics, hostKeyAlgo)
|
||||
default:
|
||||
err = fmt.Errorf("ssh: unexpected key exchange algorithm %v", kexAlgo)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = c.writePacket([]byte{msgNewKeys}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = c.transport.writer.setupKeys(clientKeys, K, H, H, hashFunc); err != nil {
|
||||
return err
|
||||
}
|
||||
if packet, err = c.readPacket(); err != nil {
|
||||
return err
|
||||
}
|
||||
if packet[0] != msgNewKeys {
|
||||
return UnexpectedMessageError{msgNewKeys, packet[0]}
|
||||
}
|
||||
if err := c.transport.reader.setupKeys(serverKeys, K, H, H, hashFunc); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.authenticate(H)
|
||||
}
|
||||
|
||||
// kexDH performs Diffie-Hellman key agreement on a ClientConn. The
|
||||
// returned values are given the same names as in RFC 4253, section 8.
|
||||
func (c *ClientConn) kexDH(group *dhGroup, hashFunc crypto.Hash, magics *handshakeMagics, hostKeyAlgo string) ([]byte, []byte, error) {
|
||||
x, err := rand.Int(c.config.rand(), group.p)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
X := new(big.Int).Exp(group.g, x, group.p)
|
||||
kexDHInit := kexDHInitMsg{
|
||||
X: X,
|
||||
}
|
||||
if err := c.writePacket(marshal(msgKexDHInit, kexDHInit)); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
packet, err := c.readPacket()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var kexDHReply = new(kexDHReplyMsg)
|
||||
if err = unmarshal(kexDHReply, packet, msgKexDHReply); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if kexDHReply.Y.Sign() == 0 || kexDHReply.Y.Cmp(group.p) >= 0 {
|
||||
return nil, nil, errors.New("server DH parameter out of bounds")
|
||||
}
|
||||
|
||||
kInt := new(big.Int).Exp(kexDHReply.Y, x, group.p)
|
||||
h := hashFunc.New()
|
||||
writeString(h, magics.clientVersion)
|
||||
writeString(h, magics.serverVersion)
|
||||
writeString(h, magics.clientKexInit)
|
||||
writeString(h, magics.serverKexInit)
|
||||
writeString(h, kexDHReply.HostKey)
|
||||
writeInt(h, X)
|
||||
writeInt(h, kexDHReply.Y)
|
||||
K := make([]byte, intLength(kInt))
|
||||
marshalInt(K, kInt)
|
||||
h.Write(K)
|
||||
|
||||
H := h.Sum(nil)
|
||||
|
||||
return H, K, nil
|
||||
}
|
||||
|
||||
// mainLoop reads incoming messages and routes channel messages
|
||||
// to their respective ClientChans.
|
||||
func (c *ClientConn) mainLoop() {
|
||||
// TODO(dfc) signal the underlying close to all channels
|
||||
defer c.Close()
|
||||
for {
|
||||
packet, err := c.readPacket()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
// TODO(dfc) A note on blocking channel use.
|
||||
// The msg, win, data and dataExt channels of a clientChan can
|
||||
// cause this loop to block indefinately if the consumer does
|
||||
// not service them.
|
||||
switch packet[0] {
|
||||
case msgChannelData:
|
||||
if len(packet) < 9 {
|
||||
// malformed data packet
|
||||
break
|
||||
}
|
||||
peersId := uint32(packet[1])<<24 | uint32(packet[2])<<16 | uint32(packet[3])<<8 | uint32(packet[4])
|
||||
if length := int(packet[5])<<24 | int(packet[6])<<16 | int(packet[7])<<8 | int(packet[8]); length > 0 {
|
||||
packet = packet[9:]
|
||||
c.getChan(peersId).stdout.handleData(packet[:length])
|
||||
}
|
||||
case msgChannelExtendedData:
|
||||
if len(packet) < 13 {
|
||||
// malformed data packet
|
||||
break
|
||||
}
|
||||
peersId := uint32(packet[1])<<24 | uint32(packet[2])<<16 | uint32(packet[3])<<8 | uint32(packet[4])
|
||||
datatype := uint32(packet[5])<<24 | uint32(packet[6])<<16 | uint32(packet[7])<<8 | uint32(packet[8])
|
||||
if length := int(packet[9])<<24 | int(packet[10])<<16 | int(packet[11])<<8 | int(packet[12]); length > 0 {
|
||||
packet = packet[13:]
|
||||
// RFC 4254 5.2 defines data_type_code 1 to be data destined
|
||||
// for stderr on interactive sessions. Other data types are
|
||||
// silently discarded.
|
||||
if datatype == 1 {
|
||||
c.getChan(peersId).stderr.handleData(packet[:length])
|
||||
}
|
||||
}
|
||||
default:
|
||||
switch msg := decode(packet).(type) {
|
||||
case *channelOpenMsg:
|
||||
c.getChan(msg.PeersId).msg <- msg
|
||||
case *channelOpenConfirmMsg:
|
||||
c.getChan(msg.PeersId).msg <- msg
|
||||
case *channelOpenFailureMsg:
|
||||
c.getChan(msg.PeersId).msg <- msg
|
||||
case *channelCloseMsg:
|
||||
ch := c.getChan(msg.PeersId)
|
||||
ch.theyClosed = true
|
||||
close(ch.stdin.win)
|
||||
ch.stdout.eof()
|
||||
ch.stderr.eof()
|
||||
close(ch.msg)
|
||||
if !ch.weClosed {
|
||||
ch.weClosed = true
|
||||
ch.sendClose()
|
||||
}
|
||||
c.chanlist.remove(msg.PeersId)
|
||||
case *channelEOFMsg:
|
||||
ch := c.getChan(msg.PeersId)
|
||||
ch.stdout.eof()
|
||||
// RFC 4254 is mute on how EOF affects dataExt messages but
|
||||
// it is logical to signal EOF at the same time.
|
||||
ch.stderr.eof()
|
||||
case *channelRequestSuccessMsg:
|
||||
c.getChan(msg.PeersId).msg <- msg
|
||||
case *channelRequestFailureMsg:
|
||||
c.getChan(msg.PeersId).msg <- msg
|
||||
case *channelRequestMsg:
|
||||
c.getChan(msg.PeersId).msg <- msg
|
||||
case *windowAdjustMsg:
|
||||
c.getChan(msg.PeersId).stdin.win <- int(msg.AdditionalBytes)
|
||||
case *disconnectMsg:
|
||||
break
|
||||
default:
|
||||
fmt.Printf("mainLoop: unhandled message %T: %v\n", msg, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dial connects to the given network address using net.Dial and
|
||||
// then initiates a SSH handshake, returning the resulting client connection.
|
||||
func Dial(network, addr string, config *ClientConfig) (*ClientConn, error) {
|
||||
conn, err := net.Dial(network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Client(conn, config)
|
||||
}
|
||||
|
||||
// A ClientConfig structure is used to configure a ClientConn. After one has
|
||||
// been passed to an SSH function it must not be modified.
|
||||
type ClientConfig struct {
|
||||
// Rand provides the source of entropy for key exchange. If Rand is
|
||||
// nil, the cryptographic random reader in package crypto/rand will
|
||||
// be used.
|
||||
Rand io.Reader
|
||||
|
||||
// The username to authenticate.
|
||||
User string
|
||||
|
||||
// A slice of ClientAuth methods. Only the first instance
|
||||
// of a particular RFC 4252 method will be used during authentication.
|
||||
Auth []ClientAuth
|
||||
|
||||
// Cryptographic-related configuration.
|
||||
Crypto CryptoConfig
|
||||
}
|
||||
|
||||
func (c *ClientConfig) rand() io.Reader {
|
||||
if c.Rand == nil {
|
||||
return rand.Reader
|
||||
}
|
||||
return c.Rand
|
||||
}
|
||||
|
||||
// A clientChan represents a single RFC 4254 channel that is multiplexed
|
||||
// over a single SSH connection.
|
||||
type clientChan struct {
|
||||
packetWriter
|
||||
id, peersId uint32
|
||||
stdin *chanWriter // receives window adjustments
|
||||
stdout *chanReader // receives the payload of channelData messages
|
||||
stderr *chanReader // receives the payload of channelExtendedData messages
|
||||
msg chan interface{} // incoming messages
|
||||
theyClosed bool // indicates the close msg has been received from the remote side
|
||||
weClosed bool // incidates the close msg has been sent from our side
|
||||
}
|
||||
|
||||
// newClientChan returns a partially constructed *clientChan
|
||||
// using the local id provided. To be usable clientChan.peersId
|
||||
// needs to be assigned once known.
|
||||
func newClientChan(t *transport, id uint32) *clientChan {
|
||||
c := &clientChan{
|
||||
packetWriter: t,
|
||||
id: id,
|
||||
msg: make(chan interface{}, 16),
|
||||
}
|
||||
c.stdin = &chanWriter{
|
||||
win: make(chan int, 16),
|
||||
clientChan: c,
|
||||
}
|
||||
c.stdout = &chanReader{
|
||||
data: make(chan []byte, 16),
|
||||
clientChan: c,
|
||||
}
|
||||
c.stderr = &chanReader{
|
||||
data: make(chan []byte, 16),
|
||||
clientChan: c,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// waitForChannelOpenResponse, if successful, fills out
|
||||
// the peerId and records any initial window advertisement.
|
||||
func (c *clientChan) waitForChannelOpenResponse() error {
|
||||
switch msg := (<-c.msg).(type) {
|
||||
case *channelOpenConfirmMsg:
|
||||
// fixup peersId field
|
||||
c.peersId = msg.MyId
|
||||
c.stdin.win <- int(msg.MyWindow)
|
||||
return nil
|
||||
case *channelOpenFailureMsg:
|
||||
return errors.New(safeString(msg.Message))
|
||||
}
|
||||
return errors.New("unexpected packet")
|
||||
}
|
||||
|
||||
// sendEOF sends EOF to the server. RFC 4254 Section 5.3
|
||||
func (c *clientChan) sendEOF() error {
|
||||
return c.writePacket(marshal(msgChannelEOF, channelEOFMsg{
|
||||
PeersId: c.peersId,
|
||||
}))
|
||||
}
|
||||
|
||||
// sendClose signals the intent to close the channel.
|
||||
func (c *clientChan) sendClose() error {
|
||||
return c.writePacket(marshal(msgChannelClose, channelCloseMsg{
|
||||
PeersId: c.peersId,
|
||||
}))
|
||||
}
|
||||
|
||||
// Close closes the channel. This does not close the underlying connection.
|
||||
func (c *clientChan) Close() error {
|
||||
if !c.weClosed {
|
||||
c.weClosed = true
|
||||
return c.sendClose()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Thread safe channel list.
|
||||
type chanlist struct {
|
||||
// protects concurrent access to chans
|
||||
sync.Mutex
|
||||
// chans are indexed by the local id of the channel, clientChan.id.
|
||||
// The PeersId value of messages received by ClientConn.mainLoop is
|
||||
// used to locate the right local clientChan in this slice.
|
||||
chans []*clientChan
|
||||
}
|
||||
|
||||
// Allocate a new ClientChan with the next avail local id.
|
||||
func (c *chanlist) newChan(t *transport) *clientChan {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
for i := range c.chans {
|
||||
if c.chans[i] == nil {
|
||||
ch := newClientChan(t, uint32(i))
|
||||
c.chans[i] = ch
|
||||
return ch
|
||||
}
|
||||
}
|
||||
i := len(c.chans)
|
||||
ch := newClientChan(t, uint32(i))
|
||||
c.chans = append(c.chans, ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *chanlist) getChan(id uint32) *clientChan {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
return c.chans[int(id)]
|
||||
}
|
||||
|
||||
func (c *chanlist) remove(id uint32) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.chans[int(id)] = nil
|
||||
}
|
||||
|
||||
// A chanWriter represents the stdin of a remote process.
|
||||
type chanWriter struct {
|
||||
win chan int // receives window adjustments
|
||||
rwin int // current rwin size
|
||||
clientChan *clientChan // the channel backing this writer
|
||||
}
|
||||
|
||||
// Write writes data to the remote process's standard input.
|
||||
func (w *chanWriter) Write(data []byte) (written int, err error) {
|
||||
for len(data) > 0 {
|
||||
for w.rwin < 1 {
|
||||
win, ok := <-w.win
|
||||
if !ok {
|
||||
return 0, io.EOF
|
||||
}
|
||||
w.rwin += win
|
||||
}
|
||||
n := min(len(data), w.rwin)
|
||||
peersId := w.clientChan.peersId
|
||||
packet := []byte{
|
||||
msgChannelData,
|
||||
byte(peersId >> 24), byte(peersId >> 16), byte(peersId >> 8), byte(peersId),
|
||||
byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n),
|
||||
}
|
||||
if err = w.clientChan.writePacket(append(packet, data[:n]...)); err != nil {
|
||||
break
|
||||
}
|
||||
data = data[n:]
|
||||
w.rwin -= n
|
||||
written += n
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (w *chanWriter) Close() error {
|
||||
return w.clientChan.sendEOF()
|
||||
}
|
||||
|
||||
// A chanReader represents stdout or stderr of a remote process.
|
||||
type chanReader struct {
|
||||
// TODO(dfc) a fixed size channel may not be the right data structure.
|
||||
// If writes to this channel block, they will block mainLoop, making
|
||||
// it unable to receive new messages from the remote side.
|
||||
data chan []byte // receives data from remote
|
||||
dataClosed bool // protects data from being closed twice
|
||||
clientChan *clientChan // the channel backing this reader
|
||||
buf []byte
|
||||
}
|
||||
|
||||
// eof signals to the consumer that there is no more data to be received.
|
||||
func (r *chanReader) eof() {
|
||||
if !r.dataClosed {
|
||||
r.dataClosed = true
|
||||
close(r.data)
|
||||
}
|
||||
}
|
||||
|
||||
// handleData sends buf to the reader's consumer. If r.data is closed
|
||||
// the data will be silently discarded
|
||||
func (r *chanReader) handleData(buf []byte) {
|
||||
if !r.dataClosed {
|
||||
r.data <- buf
|
||||
}
|
||||
}
|
||||
|
||||
// Read reads data from the remote process's stdout or stderr.
|
||||
func (r *chanReader) Read(data []byte) (int, error) {
|
||||
var ok bool
|
||||
for {
|
||||
if len(r.buf) > 0 {
|
||||
n := copy(data, r.buf)
|
||||
r.buf = r.buf[n:]
|
||||
msg := windowAdjustMsg{
|
||||
PeersId: r.clientChan.peersId,
|
||||
AdditionalBytes: uint32(n),
|
||||
}
|
||||
return n, r.clientChan.writePacket(marshal(msgChannelWindowAdjust, msg))
|
||||
}
|
||||
r.buf, ok = <-r.data
|
||||
if !ok {
|
||||
return 0, io.EOF
|
||||
}
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// authenticate authenticates with the remote server. See RFC 4252.
|
||||
func (c *ClientConn) authenticate(session []byte) error {
|
||||
// initiate user auth session
|
||||
if err := c.writePacket(marshal(msgServiceRequest, serviceRequestMsg{serviceUserAuth})); err != nil {
|
||||
return err
|
||||
}
|
||||
packet, err := c.readPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var serviceAccept serviceAcceptMsg
|
||||
if err := unmarshal(&serviceAccept, packet, msgServiceAccept); err != nil {
|
||||
return err
|
||||
}
|
||||
// during the authentication phase the client first attempts the "none" method
|
||||
// then any untried methods suggested by the server.
|
||||
tried, remain := make(map[string]bool), make(map[string]bool)
|
||||
for auth := ClientAuth(new(noneAuth)); auth != nil; {
|
||||
ok, methods, err := auth.auth(session, c.config.User, c.transport, c.config.rand())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
// success
|
||||
return nil
|
||||
}
|
||||
tried[auth.method()] = true
|
||||
delete(remain, auth.method())
|
||||
for _, meth := range methods {
|
||||
if tried[meth] {
|
||||
// if we've tried meth already, skip it.
|
||||
continue
|
||||
}
|
||||
remain[meth] = true
|
||||
}
|
||||
auth = nil
|
||||
for _, a := range c.config.Auth {
|
||||
if remain[a.method()] {
|
||||
auth = a
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.New("ssh: unable to authenticate, no supported methods remain")
|
||||
}
|
||||
|
||||
// A ClientAuth represents an instance of an RFC 4252 authentication method.
|
||||
type ClientAuth interface {
|
||||
// auth authenticates user over transport t.
|
||||
// Returns true if authentication is successful.
|
||||
// If authentication is not successful, a []string of alternative
|
||||
// method names is returned.
|
||||
auth(session []byte, user string, t *transport, rand io.Reader) (bool, []string, error)
|
||||
|
||||
// method returns the RFC 4252 method name.
|
||||
method() string
|
||||
}
|
||||
|
||||
// "none" authentication, RFC 4252 section 5.2.
|
||||
type noneAuth int
|
||||
|
||||
func (n *noneAuth) auth(session []byte, user string, t *transport, rand io.Reader) (bool, []string, error) {
|
||||
if err := t.writePacket(marshal(msgUserAuthRequest, userAuthRequestMsg{
|
||||
User: user,
|
||||
Service: serviceSSH,
|
||||
Method: "none",
|
||||
})); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
return handleAuthResponse(t)
|
||||
}
|
||||
|
||||
func (n *noneAuth) method() string {
|
||||
return "none"
|
||||
}
|
||||
|
||||
// "password" authentication, RFC 4252 Section 8.
|
||||
type passwordAuth struct {
|
||||
ClientPassword
|
||||
}
|
||||
|
||||
func (p *passwordAuth) auth(session []byte, user string, t *transport, rand io.Reader) (bool, []string, error) {
|
||||
type passwordAuthMsg struct {
|
||||
User string
|
||||
Service string
|
||||
Method string
|
||||
Reply bool
|
||||
Password string
|
||||
}
|
||||
|
||||
pw, err := p.Password(user)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
if err := t.writePacket(marshal(msgUserAuthRequest, passwordAuthMsg{
|
||||
User: user,
|
||||
Service: serviceSSH,
|
||||
Method: "password",
|
||||
Reply: false,
|
||||
Password: pw,
|
||||
})); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
return handleAuthResponse(t)
|
||||
}
|
||||
|
||||
func (p *passwordAuth) method() string {
|
||||
return "password"
|
||||
}
|
||||
|
||||
// A ClientPassword implements access to a client's passwords.
|
||||
type ClientPassword interface {
|
||||
// Password returns the password to use for user.
|
||||
Password(user string) (password string, err error)
|
||||
}
|
||||
|
||||
// ClientAuthPassword returns a ClientAuth using password authentication.
|
||||
func ClientAuthPassword(impl ClientPassword) ClientAuth {
|
||||
return &passwordAuth{impl}
|
||||
}
|
||||
|
||||
// ClientKeyring implements access to a client key ring.
|
||||
type ClientKeyring interface {
|
||||
// Key returns the i'th rsa.Publickey or dsa.Publickey, or nil if
|
||||
// no key exists at i.
|
||||
Key(i int) (key interface{}, err error)
|
||||
|
||||
// Sign returns a signature of the given data using the i'th key
|
||||
// and the supplied random source.
|
||||
Sign(i int, rand io.Reader, data []byte) (sig []byte, err error)
|
||||
}
|
||||
|
||||
// "publickey" authentication, RFC 4252 Section 7.
|
||||
type publickeyAuth struct {
|
||||
ClientKeyring
|
||||
}
|
||||
|
||||
type publickeyAuthMsg struct {
|
||||
User string
|
||||
Service string
|
||||
Method string
|
||||
// HasSig indicates to the reciver packet that the auth request is signed and
|
||||
// should be used for authentication of the request.
|
||||
HasSig bool
|
||||
Algoname string
|
||||
Pubkey string
|
||||
// Sig is defined as []byte so marshal will exclude it during validateKey
|
||||
Sig []byte `ssh:"rest"`
|
||||
}
|
||||
|
||||
func (p *publickeyAuth) auth(session []byte, user string, t *transport, 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.
|
||||
|
||||
var index int
|
||||
// a map of public keys to their index in the keyring
|
||||
validKeys := make(map[int]interface{})
|
||||
for {
|
||||
key, err := p.Key(index)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if key == nil {
|
||||
// no more keys in the keyring
|
||||
break
|
||||
}
|
||||
|
||||
if ok, err := p.validateKey(key, user, t); ok {
|
||||
validKeys[index] = key
|
||||
} else {
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
}
|
||||
index++
|
||||
}
|
||||
|
||||
// methods that may continue if this auth is not successful.
|
||||
var methods []string
|
||||
for i, key := range validKeys {
|
||||
pubkey := serializePublickey(key)
|
||||
algoname := algoName(key)
|
||||
sign, err := p.Sign(i, rand, buildDataSignedForAuth(session, userAuthRequestMsg{
|
||||
User: user,
|
||||
Service: serviceSSH,
|
||||
Method: p.method(),
|
||||
}, []byte(algoname), pubkey))
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
// manually wrap the serialized signature in a string
|
||||
s := serializeSignature(algoname, sign)
|
||||
sig := make([]byte, stringLength(s))
|
||||
marshalString(sig, s)
|
||||
msg := publickeyAuthMsg{
|
||||
User: user,
|
||||
Service: serviceSSH,
|
||||
Method: p.method(),
|
||||
HasSig: true,
|
||||
Algoname: algoname,
|
||||
Pubkey: string(pubkey),
|
||||
Sig: sig,
|
||||
}
|
||||
p := marshal(msgUserAuthRequest, msg)
|
||||
if err := t.writePacket(p); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
success, methods, err := handleAuthResponse(t)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if success {
|
||||
return success, methods, err
|
||||
}
|
||||
}
|
||||
return false, methods, nil
|
||||
}
|
||||
|
||||
// validateKey validates the key provided it is acceptable to the server.
|
||||
func (p *publickeyAuth) validateKey(key interface{}, user string, t *transport) (bool, error) {
|
||||
pubkey := serializePublickey(key)
|
||||
algoname := algoName(key)
|
||||
msg := publickeyAuthMsg{
|
||||
User: user,
|
||||
Service: serviceSSH,
|
||||
Method: p.method(),
|
||||
HasSig: false,
|
||||
Algoname: algoname,
|
||||
Pubkey: string(pubkey),
|
||||
}
|
||||
if err := t.writePacket(marshal(msgUserAuthRequest, msg)); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return p.confirmKeyAck(key, t)
|
||||
}
|
||||
|
||||
func (p *publickeyAuth) confirmKeyAck(key interface{}, t *transport) (bool, error) {
|
||||
pubkey := serializePublickey(key)
|
||||
algoname := algoName(key)
|
||||
|
||||
for {
|
||||
packet, err := t.readPacket()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
switch packet[0] {
|
||||
case msgUserAuthBanner:
|
||||
// TODO(gpaul): add callback to present the banner to the user
|
||||
case msgUserAuthPubKeyOk:
|
||||
msg := decode(packet).(*userAuthPubKeyOkMsg)
|
||||
if msg.Algo != algoname || msg.PubKey != string(pubkey) {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
case msgUserAuthFailure:
|
||||
return false, nil
|
||||
default:
|
||||
return false, UnexpectedMessageError{msgUserAuthSuccess, packet[0]}
|
||||
}
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func (p *publickeyAuth) method() string {
|
||||
return "publickey"
|
||||
}
|
||||
|
||||
// ClientAuthKeyring returns a ClientAuth using public key authentication.
|
||||
func ClientAuthKeyring(impl ClientKeyring) ClientAuth {
|
||||
return &publickeyAuth{impl}
|
||||
}
|
||||
|
||||
// handleAuthResponse returns whether the preceding authentication request succeeded
|
||||
// along with a list of remaining authentication methods to try next and
|
||||
// an error if an unexpected response was received.
|
||||
func handleAuthResponse(t *transport) (bool, []string, error) {
|
||||
for {
|
||||
packet, err := t.readPacket()
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
switch packet[0] {
|
||||
case msgUserAuthBanner:
|
||||
// TODO: add callback to present the banner to the user
|
||||
case msgUserAuthFailure:
|
||||
msg := decode(packet).(*userAuthFailureMsg)
|
||||
return false, msg.Methods, nil
|
||||
case msgUserAuthSuccess:
|
||||
return true, nil, nil
|
||||
case msgDisconnect:
|
||||
return false, nil, io.EOF
|
||||
default:
|
||||
return false, nil, UnexpectedMessageError{msgUserAuthSuccess, packet[0]}
|
||||
}
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/dsa"
|
||||
"crypto/rsa"
|
||||
_ "crypto/sha1"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// private key for mock server
|
||||
const testServerPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA19lGVsTqIT5iiNYRgnoY1CwkbETW5cq+Rzk5v/kTlf31XpSU
|
||||
70HVWkbTERECjaYdXM2gGcbb+sxpq6GtXf1M3kVomycqhxwhPv4Cr6Xp4WT/jkFx
|
||||
9z+FFzpeodGJWjOH6L2H5uX1Cvr9EDdQp9t9/J32/qBFntY8GwoUI/y/1MSTmMiF
|
||||
tupdMODN064vd3gyMKTwrlQ8tZM6aYuyOPsutLlUY7M5x5FwMDYvnPDSeyT/Iw0z
|
||||
s3B+NCyqeeMd2T7YzQFnRATj0M7rM5LoSs7DVqVriOEABssFyLj31PboaoLhOKgc
|
||||
qoM9khkNzr7FHVvi+DhYM2jD0DwvqZLN6NmnLwIDAQABAoIBAQCGVj+kuSFOV1lT
|
||||
+IclQYA6bM6uY5mroqcSBNegVxCNhWU03BxlW//BE9tA/+kq53vWylMeN9mpGZea
|
||||
riEMIh25KFGWXqXlOOioH8bkMsqA8S7sBmc7jljyv+0toQ9vCCtJ+sueNPhxQQxH
|
||||
D2YvUjfzBQ04I9+wn30BByDJ1QA/FoPsunxIOUCcRBE/7jxuLYcpR+JvEF68yYIh
|
||||
atXRld4W4in7T65YDR8jK1Uj9XAcNeDYNpT/M6oFLx1aPIlkG86aCWRO19S1jLPT
|
||||
b1ZAKHHxPMCVkSYW0RqvIgLXQOR62D0Zne6/2wtzJkk5UCjkSQ2z7ZzJpMkWgDgN
|
||||
ifCULFPBAoGBAPoMZ5q1w+zB+knXUD33n1J+niN6TZHJulpf2w5zsW+m2K6Zn62M
|
||||
MXndXlVAHtk6p02q9kxHdgov34Uo8VpuNjbS1+abGFTI8NZgFo+bsDxJdItemwC4
|
||||
KJ7L1iz39hRN/ZylMRLz5uTYRGddCkeIHhiG2h7zohH/MaYzUacXEEy3AoGBANz8
|
||||
e/msleB+iXC0cXKwds26N4hyMdAFE5qAqJXvV3S2W8JZnmU+sS7vPAWMYPlERPk1
|
||||
D8Q2eXqdPIkAWBhrx4RxD7rNc5qFNcQWEhCIxC9fccluH1y5g2M+4jpMX2CT8Uv+
|
||||
3z+NoJ5uDTXZTnLCfoZzgZ4nCZVZ+6iU5U1+YXFJAoGBANLPpIV920n/nJmmquMj
|
||||
orI1R/QXR9Cy56cMC65agezlGOfTYxk5Cfl5Ve+/2IJCfgzwJyjWUsFx7RviEeGw
|
||||
64o7JoUom1HX+5xxdHPsyZ96OoTJ5RqtKKoApnhRMamau0fWydH1yeOEJd+TRHhc
|
||||
XStGfhz8QNa1dVFvENczja1vAoGABGWhsd4VPVpHMc7lUvrf4kgKQtTC2PjA4xoc
|
||||
QJ96hf/642sVE76jl+N6tkGMzGjnVm4P2j+bOy1VvwQavKGoXqJBRd5Apppv727g
|
||||
/SM7hBXKFc/zH80xKBBgP/i1DR7kdjakCoeu4ngeGywvu2jTS6mQsqzkK+yWbUxJ
|
||||
I7mYBsECgYB/KNXlTEpXtz/kwWCHFSYA8U74l7zZbVD8ul0e56JDK+lLcJ0tJffk
|
||||
gqnBycHj6AhEycjda75cs+0zybZvN4x65KZHOGW/O/7OAWEcZP5TPb3zf9ned3Hl
|
||||
NsZoFj52ponUM6+99A2CmezFCN16c4mbA//luWF+k3VVqR6BpkrhKw==
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
|
||||
const testClientPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIBOwIBAAJBALdGZxkXDAjsYk10ihwU6Id2KeILz1TAJuoq4tOgDWxEEGeTrcld
|
||||
r/ZwVaFzjWzxaf6zQIJbfaSEAhqD5yo72+sCAwEAAQJBAK8PEVU23Wj8mV0QjwcJ
|
||||
tZ4GcTUYQL7cF4+ezTCE9a1NrGnCP2RuQkHEKxuTVrxXt+6OF15/1/fuXnxKjmJC
|
||||
nxkCIQDaXvPPBi0c7vAxGwNY9726x01/dNbHCE0CBtcotobxpwIhANbbQbh3JHVW
|
||||
2haQh4fAG5mhesZKAGcxTyv4mQ7uMSQdAiAj+4dzMpJWdSzQ+qGHlHMIBvVHLkqB
|
||||
y2VdEyF7DPCZewIhAI7GOI/6LDIFOvtPo6Bj2nNmyQ1HU6k/LRtNIXi4c9NJAiAr
|
||||
rrxx26itVhJmcvoUhOjwuzSlP2bE5VHAvkGB352YBg==
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
|
||||
// keychain implements the ClientPublickey interface
|
||||
type keychain struct {
|
||||
keys []interface{}
|
||||
}
|
||||
|
||||
func (k *keychain) Key(i int) (interface{}, error) {
|
||||
if i < 0 || i >= len(k.keys) {
|
||||
return nil, nil
|
||||
}
|
||||
switch key := k.keys[i].(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return key.PublicKey, nil
|
||||
case *dsa.PrivateKey:
|
||||
return key.PublicKey, nil
|
||||
}
|
||||
panic("unknown key type")
|
||||
}
|
||||
|
||||
func (k *keychain) Sign(i int, rand io.Reader, data []byte) (sig []byte, err error) {
|
||||
hashFunc := crypto.SHA1
|
||||
h := hashFunc.New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
switch key := k.keys[i].(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return rsa.SignPKCS1v15(rand, key, hashFunc, digest)
|
||||
}
|
||||
return nil, errors.New("unknown key type")
|
||||
}
|
||||
|
||||
func (k *keychain) loadPEM(file string) error {
|
||||
buf, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
block, _ := pem.Decode(buf)
|
||||
if block == nil {
|
||||
return errors.New("ssh: no key found")
|
||||
}
|
||||
r, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
k.keys = append(k.keys, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
// password implements the ClientPassword interface
|
||||
type password string
|
||||
|
||||
func (p password) Password(user string) (string, error) {
|
||||
return string(p), nil
|
||||
}
|
||||
|
||||
// reused internally by tests
|
||||
var (
|
||||
rsakey *rsa.PrivateKey
|
||||
dsakey *dsa.PrivateKey
|
||||
clientKeychain = new(keychain)
|
||||
clientPassword = password("tiger")
|
||||
serverConfig = &ServerConfig{
|
||||
PasswordCallback: func(user, pass string) bool {
|
||||
return user == "testuser" && pass == string(clientPassword)
|
||||
},
|
||||
PublicKeyCallback: func(user, algo string, pubkey []byte) bool {
|
||||
key := clientKeychain.keys[0].(*rsa.PrivateKey).PublicKey
|
||||
expected := []byte(serializePublickey(key))
|
||||
algoname := algoName(key)
|
||||
return user == "testuser" && algo == algoname && bytes.Equal(pubkey, expected)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := serverConfig.SetRSAPrivateKey([]byte(testServerPrivateKey)); err != nil {
|
||||
panic("unable to set private key: " + err.Error())
|
||||
}
|
||||
|
||||
block, _ := pem.Decode([]byte(testClientPrivateKey))
|
||||
rsakey, _ = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
|
||||
clientKeychain.keys = append(clientKeychain.keys, rsakey)
|
||||
dsakey = new(dsa.PrivateKey)
|
||||
// taken from crypto/dsa/dsa_test.go
|
||||
dsakey.P, _ = new(big.Int).SetString("A9B5B793FB4785793D246BAE77E8FF63CA52F442DA763C440259919FE1BC1D6065A9350637A04F75A2F039401D49F08E066C4D275A5A65DA5684BC563C14289D7AB8A67163BFBF79D85972619AD2CFF55AB0EE77A9002B0EF96293BDD0F42685EBB2C66C327079F6C98000FBCB79AACDE1BC6F9D5C7B1A97E3D9D54ED7951FEF", 16)
|
||||
dsakey.Q, _ = new(big.Int).SetString("E1D3391245933D68A0714ED34BBCB7A1F422B9C1", 16)
|
||||
dsakey.G, _ = new(big.Int).SetString("634364FC25248933D01D1993ECABD0657CC0CB2CEED7ED2E3E8AECDFCDC4A25C3B15E9E3B163ACA2984B5539181F3EFF1A5E8903D71D5B95DA4F27202B77D2C44B430BB53741A8D59A8F86887525C9F2A6A5980A195EAA7F2FF910064301DEF89D3AA213E1FAC7768D89365318E370AF54A112EFBA9246D9158386BA1B4EEFDA", 16)
|
||||
dsakey.Y, _ = new(big.Int).SetString("32969E5780CFE1C849A1C276D7AEB4F38A23B591739AA2FE197349AEEBD31366AEE5EB7E6C6DDB7C57D02432B30DB5AA66D9884299FAA72568944E4EEDC92EA3FBC6F39F53412FBCC563208F7C15B737AC8910DBC2D9C9B8C001E72FDC40EB694AB1F06A5A2DBD18D9E36C66F31F566742F11EC0A52E9F7B89355C02FB5D32D2", 16)
|
||||
dsakey.X, _ = new(big.Int).SetString("5078D4D29795CBE76D3AACFE48C9AF0BCDBEE91A", 16)
|
||||
}
|
||||
|
||||
// newMockAuthServer creates a new Server bound to
|
||||
// the loopback interface. The server exits after
|
||||
// processing one handshake.
|
||||
func newMockAuthServer(t *testing.T) string {
|
||||
l, err := Listen("tcp", "127.0.0.1:0", serverConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to newMockAuthServer: %s", err)
|
||||
}
|
||||
go func() {
|
||||
defer l.Close()
|
||||
c, err := l.Accept()
|
||||
defer c.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Unable to accept incoming connection: %v", err)
|
||||
return
|
||||
}
|
||||
if err := c.Handshake(); err != nil {
|
||||
// not Errorf because this is expected to
|
||||
// fail for some tests.
|
||||
t.Logf("Handshaking error: %v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
return l.Addr().String()
|
||||
}
|
||||
|
||||
func TestClientAuthPublickey(t *testing.T) {
|
||||
config := &ClientConfig{
|
||||
User: "testuser",
|
||||
Auth: []ClientAuth{
|
||||
ClientAuthKeyring(clientKeychain),
|
||||
},
|
||||
}
|
||||
c, err := Dial("tcp", newMockAuthServer(t), config)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to dial remote side: %s", err)
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
|
||||
func TestClientAuthPassword(t *testing.T) {
|
||||
config := &ClientConfig{
|
||||
User: "testuser",
|
||||
Auth: []ClientAuth{
|
||||
ClientAuthPassword(clientPassword),
|
||||
},
|
||||
}
|
||||
|
||||
c, err := Dial("tcp", newMockAuthServer(t), config)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to dial remote side: %s", err)
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
|
||||
func TestClientAuthWrongPassword(t *testing.T) {
|
||||
wrongPw := password("wrong")
|
||||
config := &ClientConfig{
|
||||
User: "testuser",
|
||||
Auth: []ClientAuth{
|
||||
ClientAuthPassword(wrongPw),
|
||||
ClientAuthKeyring(clientKeychain),
|
||||
},
|
||||
}
|
||||
|
||||
c, err := Dial("tcp", newMockAuthServer(t), config)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to dial remote side: %s", err)
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
|
||||
// the mock server will only authenticate ssh-rsa keys
|
||||
func TestClientAuthInvalidPublickey(t *testing.T) {
|
||||
kc := new(keychain)
|
||||
kc.keys = append(kc.keys, dsakey)
|
||||
config := &ClientConfig{
|
||||
User: "testuser",
|
||||
Auth: []ClientAuth{
|
||||
ClientAuthKeyring(kc),
|
||||
},
|
||||
}
|
||||
|
||||
c, err := Dial("tcp", newMockAuthServer(t), config)
|
||||
if err == nil {
|
||||
c.Close()
|
||||
t.Fatalf("dsa private key should not have authenticated with rsa public key")
|
||||
}
|
||||
}
|
||||
|
||||
// the client should authenticate with the second key
|
||||
func TestClientAuthRSAandDSA(t *testing.T) {
|
||||
kc := new(keychain)
|
||||
kc.keys = append(kc.keys, dsakey, rsakey)
|
||||
config := &ClientConfig{
|
||||
User: "testuser",
|
||||
Auth: []ClientAuth{
|
||||
ClientAuthKeyring(kc),
|
||||
},
|
||||
}
|
||||
c, err := Dial("tcp", newMockAuthServer(t), config)
|
||||
if err != nil {
|
||||
t.Fatalf("client could not authenticate with rsa key: %v", err)
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
// ClientConn functional tests.
|
||||
// These tests require a running ssh server listening on port 22
|
||||
// on the local host. Functional tests will be skipped unless
|
||||
// -ssh.user and -ssh.pass must be passed to gotest.
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
sshuser = flag.String("ssh.user", "", "ssh username")
|
||||
sshpass = flag.String("ssh.pass", "", "ssh password")
|
||||
sshprivkey = flag.String("ssh.privkey", "", "ssh privkey file")
|
||||
)
|
||||
|
||||
func TestFuncPasswordAuth(t *testing.T) {
|
||||
if *sshuser == "" {
|
||||
t.Log("ssh.user not defined, skipping test")
|
||||
return
|
||||
}
|
||||
config := &ClientConfig{
|
||||
User: *sshuser,
|
||||
Auth: []ClientAuth{
|
||||
ClientAuthPassword(password(*sshpass)),
|
||||
},
|
||||
}
|
||||
conn, err := Dial("tcp", "localhost:22", config)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to connect: %s", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
}
|
||||
|
||||
func TestFuncPublickeyAuth(t *testing.T) {
|
||||
if *sshuser == "" {
|
||||
t.Log("ssh.user not defined, skipping test")
|
||||
return
|
||||
}
|
||||
kc := new(keychain)
|
||||
if err := kc.loadPEM(*sshprivkey); err != nil {
|
||||
t.Fatalf("unable to load private key: %s", err)
|
||||
}
|
||||
config := &ClientConfig{
|
||||
User: *sshuser,
|
||||
Auth: []ClientAuth{
|
||||
ClientAuthKeyring(kc),
|
||||
},
|
||||
}
|
||||
conn, err := Dial("tcp", "localhost:22", config)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to connect: %s", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
import (
|
||||
"crypto/dsa"
|
||||
"crypto/rsa"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// These are string constants in the SSH protocol.
|
||||
const (
|
||||
kexAlgoDH14SHA1 = "diffie-hellman-group14-sha1"
|
||||
hostAlgoRSA = "ssh-rsa"
|
||||
macSHA196 = "hmac-sha1-96"
|
||||
compressionNone = "none"
|
||||
serviceUserAuth = "ssh-userauth"
|
||||
serviceSSH = "ssh-connection"
|
||||
)
|
||||
|
||||
var supportedKexAlgos = []string{kexAlgoDH14SHA1}
|
||||
var supportedHostKeyAlgos = []string{hostAlgoRSA}
|
||||
var supportedMACs = []string{macSHA196}
|
||||
var supportedCompressions = []string{compressionNone}
|
||||
|
||||
// dhGroup is a multiplicative group suitable for implementing Diffie-Hellman key agreement.
|
||||
type dhGroup struct {
|
||||
g, p *big.Int
|
||||
}
|
||||
|
||||
// dhGroup14 is the group called diffie-hellman-group14-sha1 in RFC 4253 and
|
||||
// Oakley Group 14 in RFC 3526.
|
||||
var dhGroup14 *dhGroup
|
||||
|
||||
var dhGroup14Once sync.Once
|
||||
|
||||
func initDHGroup14() {
|
||||
p, _ := new(big.Int).SetString("FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF", 16)
|
||||
|
||||
dhGroup14 = &dhGroup{
|
||||
g: new(big.Int).SetInt64(2),
|
||||
p: p,
|
||||
}
|
||||
}
|
||||
|
||||
// UnexpectedMessageError results when the SSH message that we received didn't
|
||||
// match what we wanted.
|
||||
type UnexpectedMessageError struct {
|
||||
expected, got uint8
|
||||
}
|
||||
|
||||
func (u UnexpectedMessageError) Error() string {
|
||||
return "ssh: unexpected message type " + strconv.Itoa(int(u.got)) + " (expected " + strconv.Itoa(int(u.expected)) + ")"
|
||||
}
|
||||
|
||||
// ParseError results from a malformed SSH message.
|
||||
type ParseError struct {
|
||||
msgType uint8
|
||||
}
|
||||
|
||||
func (p ParseError) Error() string {
|
||||
return "ssh: parse error in message type " + strconv.Itoa(int(p.msgType))
|
||||
}
|
||||
|
||||
type handshakeMagics struct {
|
||||
clientVersion, serverVersion []byte
|
||||
clientKexInit, serverKexInit []byte
|
||||
}
|
||||
|
||||
func findCommonAlgorithm(clientAlgos []string, serverAlgos []string) (commonAlgo string, ok bool) {
|
||||
for _, clientAlgo := range clientAlgos {
|
||||
for _, serverAlgo := range serverAlgos {
|
||||
if clientAlgo == serverAlgo {
|
||||
return clientAlgo, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func findAgreedAlgorithms(transport *transport, clientKexInit, serverKexInit *kexInitMsg) (kexAlgo, hostKeyAlgo string, ok bool) {
|
||||
kexAlgo, ok = findCommonAlgorithm(clientKexInit.KexAlgos, serverKexInit.KexAlgos)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
hostKeyAlgo, ok = findCommonAlgorithm(clientKexInit.ServerHostKeyAlgos, serverKexInit.ServerHostKeyAlgos)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
transport.writer.cipherAlgo, ok = findCommonAlgorithm(clientKexInit.CiphersClientServer, serverKexInit.CiphersClientServer)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
transport.reader.cipherAlgo, ok = findCommonAlgorithm(clientKexInit.CiphersServerClient, serverKexInit.CiphersServerClient)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
transport.writer.macAlgo, ok = findCommonAlgorithm(clientKexInit.MACsClientServer, serverKexInit.MACsClientServer)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
transport.reader.macAlgo, ok = findCommonAlgorithm(clientKexInit.MACsServerClient, serverKexInit.MACsServerClient)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
transport.writer.compressionAlgo, ok = findCommonAlgorithm(clientKexInit.CompressionClientServer, serverKexInit.CompressionClientServer)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
transport.reader.compressionAlgo, ok = findCommonAlgorithm(clientKexInit.CompressionServerClient, serverKexInit.CompressionServerClient)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
// Cryptographic configuration common to both ServerConfig and ClientConfig.
|
||||
type CryptoConfig struct {
|
||||
// The allowed cipher algorithms. If unspecified then DefaultCipherOrder is
|
||||
// used.
|
||||
Ciphers []string
|
||||
}
|
||||
|
||||
func (c *CryptoConfig) ciphers() []string {
|
||||
if c.Ciphers == nil {
|
||||
return DefaultCipherOrder
|
||||
}
|
||||
return c.Ciphers
|
||||
}
|
||||
|
||||
// serialize a signed slice according to RFC 4254 6.6.
|
||||
func serializeSignature(algoname string, sig []byte) []byte {
|
||||
length := stringLength([]byte(algoname))
|
||||
length += stringLength(sig)
|
||||
|
||||
ret := make([]byte, length)
|
||||
r := marshalString(ret, []byte(algoname))
|
||||
r = marshalString(r, sig)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// serialize an rsa.PublicKey or dsa.PublicKey according to RFC 4253 6.6.
|
||||
func serializePublickey(key interface{}) []byte {
|
||||
algoname := algoName(key)
|
||||
switch key := key.(type) {
|
||||
case rsa.PublicKey:
|
||||
e := new(big.Int).SetInt64(int64(key.E))
|
||||
length := stringLength([]byte(algoname))
|
||||
length += intLength(e)
|
||||
length += intLength(key.N)
|
||||
ret := make([]byte, length)
|
||||
r := marshalString(ret, []byte(algoname))
|
||||
r = marshalInt(r, e)
|
||||
marshalInt(r, key.N)
|
||||
return ret
|
||||
case dsa.PublicKey:
|
||||
length := stringLength([]byte(algoname))
|
||||
length += intLength(key.P)
|
||||
length += intLength(key.Q)
|
||||
length += intLength(key.G)
|
||||
length += intLength(key.Y)
|
||||
ret := make([]byte, length)
|
||||
r := marshalString(ret, []byte(algoname))
|
||||
r = marshalInt(r, key.P)
|
||||
r = marshalInt(r, key.Q)
|
||||
r = marshalInt(r, key.G)
|
||||
marshalInt(r, key.Y)
|
||||
return ret
|
||||
}
|
||||
panic("unexpected key type")
|
||||
}
|
||||
|
||||
func algoName(key interface{}) string {
|
||||
switch key.(type) {
|
||||
case rsa.PublicKey:
|
||||
return "ssh-rsa"
|
||||
case dsa.PublicKey:
|
||||
return "ssh-dss"
|
||||
}
|
||||
panic("unexpected key type")
|
||||
}
|
||||
|
||||
// buildDataSignedForAuth returns the data that is signed in order to prove
|
||||
// posession of a private key. See RFC 4252, section 7.
|
||||
func buildDataSignedForAuth(sessionId []byte, req userAuthRequestMsg, algo, pubKey []byte) []byte {
|
||||
user := []byte(req.User)
|
||||
service := []byte(req.Service)
|
||||
method := []byte(req.Method)
|
||||
|
||||
length := stringLength(sessionId)
|
||||
length += 1
|
||||
length += stringLength(user)
|
||||
length += stringLength(service)
|
||||
length += stringLength(method)
|
||||
length += 1
|
||||
length += stringLength(algo)
|
||||
length += stringLength(pubKey)
|
||||
|
||||
ret := make([]byte, length)
|
||||
r := marshalString(ret, sessionId)
|
||||
r[0] = msgUserAuthRequest
|
||||
r = r[1:]
|
||||
r = marshalString(r, user)
|
||||
r = marshalString(r, service)
|
||||
r = marshalString(r, method)
|
||||
r[0] = 1
|
||||
r = r[1:]
|
||||
r = marshalString(r, algo)
|
||||
r = marshalString(r, pubKey)
|
||||
return ret
|
||||
}
|
||||
|
||||
// safeString sanitises s according to RFC 4251, section 9.2.
|
||||
// All control characters except tab, carriage return and newline are
|
||||
// replaced by 0x20.
|
||||
func safeString(s string) string {
|
||||
out := []byte(s)
|
||||
for i, c := range out {
|
||||
if c < 0x20 && c != 0xd && c != 0xa && c != 0x9 {
|
||||
out[i] = 0x20
|
||||
}
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSafeString(t *testing.T) {
|
||||
strings := map[string]string{
|
||||
"\x20\x0d\x0a": "\x20\x0d\x0a",
|
||||
"flibble": "flibble",
|
||||
"new\x20line": "new\x20line",
|
||||
"123456\x07789": "123456 789",
|
||||
"\t\t\x10\r\n": "\t\t \r\n",
|
||||
}
|
||||
|
||||
for s, expected := range strings {
|
||||
actual := safeString(s)
|
||||
if expected != actual {
|
||||
t.Errorf("expected: %v, actual: %v", []byte(expected), []byte(actual))
|
||||
}
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
// Copyright 2011 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 ssh implements an SSH client and server.
|
||||
|
||||
SSH is a transport security protocol, an authentication protocol and a
|
||||
family of application protocols. The most typical application level
|
||||
protocol is a remote shell and this is specifically implemented. However,
|
||||
the multiplexed nature of SSH is exposed to users that wish to support
|
||||
others.
|
||||
|
||||
An SSH server is represented by a ServerConfig, which holds certificate
|
||||
details and handles authentication of ServerConns.
|
||||
|
||||
config := new(ssh.ServerConfig)
|
||||
config.PubKeyCallback = pubKeyAuth
|
||||
config.PasswordCallback = passwordAuth
|
||||
|
||||
pemBytes, err := ioutil.ReadFile("id_rsa")
|
||||
if err != nil {
|
||||
panic("Failed to load private key")
|
||||
}
|
||||
err = config.SetRSAPrivateKey(pemBytes)
|
||||
if err != nil {
|
||||
panic("Failed to parse private key")
|
||||
}
|
||||
|
||||
Once a ServerConfig has been configured, connections can be accepted.
|
||||
|
||||
listener := Listen("tcp", "0.0.0.0:2022", config)
|
||||
sConn, err := listener.Accept()
|
||||
if err != nil {
|
||||
panic("failed to accept incoming connection")
|
||||
}
|
||||
if err := sConn.Handshake(conn); err != nil {
|
||||
panic("failed to handshake")
|
||||
}
|
||||
|
||||
An SSH connection multiplexes several channels, which must be accepted themselves:
|
||||
|
||||
for {
|
||||
channel, err := sConn.Accept()
|
||||
if err != nil {
|
||||
panic("error from Accept")
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
|
||||
Accept reads from the connection, demultiplexes packets to their corresponding
|
||||
channels and returns when a new channel request is seen. Some goroutine must
|
||||
always be calling Accept; otherwise no messages will be forwarded to the
|
||||
channels.
|
||||
|
||||
Channels have a type, depending on the application level protocol intended. In
|
||||
the case of a shell, the type is "session" and ServerShell may be used to
|
||||
present a simple terminal interface.
|
||||
|
||||
if channel.ChannelType() != "session" {
|
||||
channel.Reject(UnknownChannelType, "unknown channel type")
|
||||
return
|
||||
}
|
||||
channel.Accept()
|
||||
|
||||
term := terminal.NewTerminal(channel, "> ")
|
||||
serverTerm := &ssh.ServerTerminal{
|
||||
Term: term,
|
||||
Channel: channel,
|
||||
}
|
||||
go func() {
|
||||
defer channel.Close()
|
||||
for {
|
||||
line, err := serverTerm.ReadLine()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
println(line)
|
||||
}
|
||||
return
|
||||
}()
|
||||
|
||||
To authenticate with the remote server you must pass at least one implementation of
|
||||
ClientAuth via the Auth field in ClientConfig.
|
||||
|
||||
// password implements the ClientPassword interface
|
||||
type password string
|
||||
|
||||
func (p password) Password(user string) (string, error) {
|
||||
return string(p), nil
|
||||
}
|
||||
|
||||
config := &ssh.ClientConfig {
|
||||
User: "username",
|
||||
Auth: []ClientAuth {
|
||||
// ClientAuthPassword wraps a ClientPassword implementation
|
||||
// in a type that implements ClientAuth.
|
||||
ClientAuthPassword(password("yourpassword")),
|
||||
}
|
||||
}
|
||||
|
||||
An SSH client is represented with a ClientConn. Currently only the "password"
|
||||
authentication method is supported.
|
||||
|
||||
config := &ClientConfig{
|
||||
User: "username",
|
||||
Auth: []ClientAuth{ ... },
|
||||
}
|
||||
client, err := Dial("yourserver.com:22", config)
|
||||
|
||||
Each ClientConn can support multiple interactive sessions, represented by a Session.
|
||||
|
||||
session, err := client.NewSession()
|
||||
|
||||
Once a Session is created, you can execute a single command on the remote side
|
||||
using the Exec method.
|
||||
|
||||
b := bytes.NewBuffer()
|
||||
session.Stdin = b
|
||||
if err := session.Run("/usr/bin/whoami"); err != nil {
|
||||
panic("Failed to exec: " + err.String())
|
||||
}
|
||||
fmt.Println(bytes.String())
|
||||
session.Close()
|
||||
*/
|
||||
package ssh
|
||||
+640
@@ -0,0 +1,640 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"math/big"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// These are SSH message type numbers. They are scattered around several
|
||||
// documents but many were taken from
|
||||
// http://www.iana.org/assignments/ssh-parameters/ssh-parameters.xml#ssh-parameters-1
|
||||
const (
|
||||
msgDisconnect = 1
|
||||
msgIgnore = 2
|
||||
msgUnimplemented = 3
|
||||
msgDebug = 4
|
||||
msgServiceRequest = 5
|
||||
msgServiceAccept = 6
|
||||
|
||||
msgKexInit = 20
|
||||
msgNewKeys = 21
|
||||
|
||||
msgKexDHInit = 30
|
||||
msgKexDHReply = 31
|
||||
|
||||
msgUserAuthRequest = 50
|
||||
msgUserAuthFailure = 51
|
||||
msgUserAuthSuccess = 52
|
||||
msgUserAuthBanner = 53
|
||||
msgUserAuthPubKeyOk = 60
|
||||
|
||||
msgGlobalRequest = 80
|
||||
msgRequestSuccess = 81
|
||||
msgRequestFailure = 82
|
||||
|
||||
msgChannelOpen = 90
|
||||
msgChannelOpenConfirm = 91
|
||||
msgChannelOpenFailure = 92
|
||||
msgChannelWindowAdjust = 93
|
||||
msgChannelData = 94
|
||||
msgChannelExtendedData = 95
|
||||
msgChannelEOF = 96
|
||||
msgChannelClose = 97
|
||||
msgChannelRequest = 98
|
||||
msgChannelSuccess = 99
|
||||
msgChannelFailure = 100
|
||||
)
|
||||
|
||||
// SSH messages:
|
||||
//
|
||||
// These structures mirror the wire format of the corresponding SSH messages.
|
||||
// They are marshaled using reflection with the marshal and unmarshal functions
|
||||
// in this file. The only wrinkle is that a final member of type []byte with a
|
||||
// ssh tag of "rest" receives the remainder of a packet when unmarshaling.
|
||||
|
||||
// See RFC 4253, section 11.1.
|
||||
type disconnectMsg struct {
|
||||
Reason uint32
|
||||
Message string
|
||||
Language string
|
||||
}
|
||||
|
||||
// See RFC 4253, section 7.1.
|
||||
type kexInitMsg struct {
|
||||
Cookie [16]byte
|
||||
KexAlgos []string
|
||||
ServerHostKeyAlgos []string
|
||||
CiphersClientServer []string
|
||||
CiphersServerClient []string
|
||||
MACsClientServer []string
|
||||
MACsServerClient []string
|
||||
CompressionClientServer []string
|
||||
CompressionServerClient []string
|
||||
LanguagesClientServer []string
|
||||
LanguagesServerClient []string
|
||||
FirstKexFollows bool
|
||||
Reserved uint32
|
||||
}
|
||||
|
||||
// See RFC 4253, section 8.
|
||||
type kexDHInitMsg struct {
|
||||
X *big.Int
|
||||
}
|
||||
|
||||
type kexDHReplyMsg struct {
|
||||
HostKey []byte
|
||||
Y *big.Int
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
// See RFC 4253, section 10.
|
||||
type serviceRequestMsg struct {
|
||||
Service string
|
||||
}
|
||||
|
||||
// See RFC 4253, section 10.
|
||||
type serviceAcceptMsg struct {
|
||||
Service string
|
||||
}
|
||||
|
||||
// See RFC 4252, section 5.
|
||||
type userAuthRequestMsg struct {
|
||||
User string
|
||||
Service string
|
||||
Method string
|
||||
Payload []byte `ssh:"rest"`
|
||||
}
|
||||
|
||||
// See RFC 4252, section 5.1
|
||||
type userAuthFailureMsg struct {
|
||||
Methods []string
|
||||
PartialSuccess bool
|
||||
}
|
||||
|
||||
// See RFC 4254, section 5.1.
|
||||
type channelOpenMsg struct {
|
||||
ChanType string
|
||||
PeersId uint32
|
||||
PeersWindow uint32
|
||||
MaxPacketSize uint32
|
||||
TypeSpecificData []byte `ssh:"rest"`
|
||||
}
|
||||
|
||||
// See RFC 4254, section 5.1.
|
||||
type channelOpenConfirmMsg struct {
|
||||
PeersId uint32
|
||||
MyId uint32
|
||||
MyWindow uint32
|
||||
MaxPacketSize uint32
|
||||
TypeSpecificData []byte `ssh:"rest"`
|
||||
}
|
||||
|
||||
// See RFC 4254, section 5.1.
|
||||
type channelOpenFailureMsg struct {
|
||||
PeersId uint32
|
||||
Reason uint32
|
||||
Message string
|
||||
Language string
|
||||
}
|
||||
|
||||
type channelRequestMsg struct {
|
||||
PeersId uint32
|
||||
Request string
|
||||
WantReply bool
|
||||
RequestSpecificData []byte `ssh:"rest"`
|
||||
}
|
||||
|
||||
// See RFC 4254, section 5.4.
|
||||
type channelRequestSuccessMsg struct {
|
||||
PeersId uint32
|
||||
}
|
||||
|
||||
// See RFC 4254, section 5.4.
|
||||
type channelRequestFailureMsg struct {
|
||||
PeersId uint32
|
||||
}
|
||||
|
||||
// See RFC 4254, section 5.3
|
||||
type channelCloseMsg struct {
|
||||
PeersId uint32
|
||||
}
|
||||
|
||||
// See RFC 4254, section 5.3
|
||||
type channelEOFMsg struct {
|
||||
PeersId uint32
|
||||
}
|
||||
|
||||
// See RFC 4254, section 4
|
||||
type globalRequestMsg struct {
|
||||
Type string
|
||||
WantReply bool
|
||||
}
|
||||
|
||||
// See RFC 4254, section 5.2
|
||||
type windowAdjustMsg struct {
|
||||
PeersId uint32
|
||||
AdditionalBytes uint32
|
||||
}
|
||||
|
||||
// See RFC 4252, section 7
|
||||
type userAuthPubKeyOkMsg struct {
|
||||
Algo string
|
||||
PubKey string
|
||||
}
|
||||
|
||||
// unmarshal parses the SSH wire data in packet into out using reflection.
|
||||
// expectedType is the expected SSH message type. It either returns nil on
|
||||
// success, or a ParseError or UnexpectedMessageError on error.
|
||||
func unmarshal(out interface{}, packet []byte, expectedType uint8) error {
|
||||
if len(packet) == 0 {
|
||||
return ParseError{expectedType}
|
||||
}
|
||||
if packet[0] != expectedType {
|
||||
return UnexpectedMessageError{expectedType, packet[0]}
|
||||
}
|
||||
packet = packet[1:]
|
||||
|
||||
v := reflect.ValueOf(out).Elem()
|
||||
structType := v.Type()
|
||||
var ok bool
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
t := field.Type()
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
if len(packet) < 1 {
|
||||
return ParseError{expectedType}
|
||||
}
|
||||
field.SetBool(packet[0] != 0)
|
||||
packet = packet[1:]
|
||||
case reflect.Array:
|
||||
if t.Elem().Kind() != reflect.Uint8 {
|
||||
panic("array of non-uint8")
|
||||
}
|
||||
if len(packet) < t.Len() {
|
||||
return ParseError{expectedType}
|
||||
}
|
||||
for j := 0; j < t.Len(); j++ {
|
||||
field.Index(j).Set(reflect.ValueOf(packet[j]))
|
||||
}
|
||||
packet = packet[t.Len():]
|
||||
case reflect.Uint32:
|
||||
var u32 uint32
|
||||
if u32, packet, ok = parseUint32(packet); !ok {
|
||||
return ParseError{expectedType}
|
||||
}
|
||||
field.SetUint(uint64(u32))
|
||||
case reflect.String:
|
||||
var s []byte
|
||||
if s, packet, ok = parseString(packet); !ok {
|
||||
return ParseError{expectedType}
|
||||
}
|
||||
field.SetString(string(s))
|
||||
case reflect.Slice:
|
||||
switch t.Elem().Kind() {
|
||||
case reflect.Uint8:
|
||||
if structType.Field(i).Tag.Get("ssh") == "rest" {
|
||||
field.Set(reflect.ValueOf(packet))
|
||||
packet = nil
|
||||
} else {
|
||||
var s []byte
|
||||
if s, packet, ok = parseString(packet); !ok {
|
||||
return ParseError{expectedType}
|
||||
}
|
||||
field.Set(reflect.ValueOf(s))
|
||||
}
|
||||
case reflect.String:
|
||||
var nl []string
|
||||
if nl, packet, ok = parseNameList(packet); !ok {
|
||||
return ParseError{expectedType}
|
||||
}
|
||||
field.Set(reflect.ValueOf(nl))
|
||||
default:
|
||||
panic("slice of unknown type")
|
||||
}
|
||||
case reflect.Ptr:
|
||||
if t == bigIntType {
|
||||
var n *big.Int
|
||||
if n, packet, ok = parseInt(packet); !ok {
|
||||
return ParseError{expectedType}
|
||||
}
|
||||
field.Set(reflect.ValueOf(n))
|
||||
} else {
|
||||
panic("pointer to unknown type")
|
||||
}
|
||||
default:
|
||||
panic("unknown type")
|
||||
}
|
||||
}
|
||||
|
||||
if len(packet) != 0 {
|
||||
return ParseError{expectedType}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshal serializes the message in msg, using the given message type.
|
||||
func marshal(msgType uint8, msg interface{}) []byte {
|
||||
var out []byte
|
||||
out = append(out, msgType)
|
||||
|
||||
v := reflect.ValueOf(msg)
|
||||
structType := v.Type()
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
t := field.Type()
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
var v uint8
|
||||
if field.Bool() {
|
||||
v = 1
|
||||
}
|
||||
out = append(out, v)
|
||||
case reflect.Array:
|
||||
if t.Elem().Kind() != reflect.Uint8 {
|
||||
panic("array of non-uint8")
|
||||
}
|
||||
for j := 0; j < t.Len(); j++ {
|
||||
out = append(out, byte(field.Index(j).Uint()))
|
||||
}
|
||||
case reflect.Uint32:
|
||||
u32 := uint32(field.Uint())
|
||||
out = append(out, byte(u32>>24))
|
||||
out = append(out, byte(u32>>16))
|
||||
out = append(out, byte(u32>>8))
|
||||
out = append(out, byte(u32))
|
||||
case reflect.String:
|
||||
s := field.String()
|
||||
out = append(out, byte(len(s)>>24))
|
||||
out = append(out, byte(len(s)>>16))
|
||||
out = append(out, byte(len(s)>>8))
|
||||
out = append(out, byte(len(s)))
|
||||
out = append(out, s...)
|
||||
case reflect.Slice:
|
||||
switch t.Elem().Kind() {
|
||||
case reflect.Uint8:
|
||||
length := field.Len()
|
||||
if structType.Field(i).Tag.Get("ssh") != "rest" {
|
||||
out = append(out, byte(length>>24))
|
||||
out = append(out, byte(length>>16))
|
||||
out = append(out, byte(length>>8))
|
||||
out = append(out, byte(length))
|
||||
}
|
||||
for j := 0; j < length; j++ {
|
||||
out = append(out, byte(field.Index(j).Uint()))
|
||||
}
|
||||
case reflect.String:
|
||||
var length int
|
||||
for j := 0; j < field.Len(); j++ {
|
||||
if j != 0 {
|
||||
length++ /* comma */
|
||||
}
|
||||
length += len(field.Index(j).String())
|
||||
}
|
||||
|
||||
out = append(out, byte(length>>24))
|
||||
out = append(out, byte(length>>16))
|
||||
out = append(out, byte(length>>8))
|
||||
out = append(out, byte(length))
|
||||
for j := 0; j < field.Len(); j++ {
|
||||
if j != 0 {
|
||||
out = append(out, ',')
|
||||
}
|
||||
out = append(out, field.Index(j).String()...)
|
||||
}
|
||||
default:
|
||||
panic("slice of unknown type")
|
||||
}
|
||||
case reflect.Ptr:
|
||||
if t == bigIntType {
|
||||
var n *big.Int
|
||||
nValue := reflect.ValueOf(&n)
|
||||
nValue.Elem().Set(field)
|
||||
needed := intLength(n)
|
||||
oldLength := len(out)
|
||||
|
||||
if cap(out)-len(out) < needed {
|
||||
newOut := make([]byte, len(out), 2*(len(out)+needed))
|
||||
copy(newOut, out)
|
||||
out = newOut
|
||||
}
|
||||
out = out[:oldLength+needed]
|
||||
marshalInt(out[oldLength:], n)
|
||||
} else {
|
||||
panic("pointer to unknown type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
var bigOne = big.NewInt(1)
|
||||
|
||||
func parseString(in []byte) (out, rest []byte, ok bool) {
|
||||
if len(in) < 4 {
|
||||
return
|
||||
}
|
||||
length := uint32(in[0])<<24 | uint32(in[1])<<16 | uint32(in[2])<<8 | uint32(in[3])
|
||||
if uint32(len(in)) < 4+length {
|
||||
return
|
||||
}
|
||||
out = in[4 : 4+length]
|
||||
rest = in[4+length:]
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
comma = []byte{','}
|
||||
emptyNameList = []string{}
|
||||
)
|
||||
|
||||
func parseNameList(in []byte) (out []string, rest []byte, ok bool) {
|
||||
contents, rest, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if len(contents) == 0 {
|
||||
out = emptyNameList
|
||||
return
|
||||
}
|
||||
parts := bytes.Split(contents, comma)
|
||||
out = make([]string, len(parts))
|
||||
for i, part := range parts {
|
||||
out[i] = string(part)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseInt(in []byte) (out *big.Int, rest []byte, ok bool) {
|
||||
contents, rest, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
out = new(big.Int)
|
||||
|
||||
if len(contents) > 0 && contents[0]&0x80 == 0x80 {
|
||||
// This is a negative number
|
||||
notBytes := make([]byte, len(contents))
|
||||
for i := range notBytes {
|
||||
notBytes[i] = ^contents[i]
|
||||
}
|
||||
out.SetBytes(notBytes)
|
||||
out.Add(out, bigOne)
|
||||
out.Neg(out)
|
||||
} else {
|
||||
// Positive number
|
||||
out.SetBytes(contents)
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func parseUint32(in []byte) (out uint32, rest []byte, ok bool) {
|
||||
if len(in) < 4 {
|
||||
return
|
||||
}
|
||||
out = uint32(in[0])<<24 | uint32(in[1])<<16 | uint32(in[2])<<8 | uint32(in[3])
|
||||
rest = in[4:]
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func nameListLength(namelist []string) int {
|
||||
length := 4 /* uint32 length prefix */
|
||||
for i, name := range namelist {
|
||||
if i != 0 {
|
||||
length++ /* comma */
|
||||
}
|
||||
length += len(name)
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
func intLength(n *big.Int) int {
|
||||
length := 4 /* length bytes */
|
||||
if n.Sign() < 0 {
|
||||
nMinus1 := new(big.Int).Neg(n)
|
||||
nMinus1.Sub(nMinus1, bigOne)
|
||||
bitLen := nMinus1.BitLen()
|
||||
if bitLen%8 == 0 {
|
||||
// The number will need 0xff padding
|
||||
length++
|
||||
}
|
||||
length += (bitLen + 7) / 8
|
||||
} else if n.Sign() == 0 {
|
||||
// A zero is the zero length string
|
||||
} else {
|
||||
bitLen := n.BitLen()
|
||||
if bitLen%8 == 0 {
|
||||
// The number will need 0x00 padding
|
||||
length++
|
||||
}
|
||||
length += (bitLen + 7) / 8
|
||||
}
|
||||
|
||||
return length
|
||||
}
|
||||
|
||||
func marshalUint32(to []byte, n uint32) []byte {
|
||||
to[0] = byte(n >> 24)
|
||||
to[1] = byte(n >> 16)
|
||||
to[2] = byte(n >> 8)
|
||||
to[3] = byte(n)
|
||||
return to[4:]
|
||||
}
|
||||
|
||||
func marshalUint64(to []byte, n uint64) []byte {
|
||||
to[0] = byte(n >> 56)
|
||||
to[1] = byte(n >> 48)
|
||||
to[2] = byte(n >> 40)
|
||||
to[3] = byte(n >> 32)
|
||||
to[4] = byte(n >> 24)
|
||||
to[5] = byte(n >> 16)
|
||||
to[6] = byte(n >> 8)
|
||||
to[7] = byte(n)
|
||||
return to[8:]
|
||||
}
|
||||
|
||||
func marshalInt(to []byte, n *big.Int) []byte {
|
||||
lengthBytes := to
|
||||
to = to[4:]
|
||||
length := 0
|
||||
|
||||
if n.Sign() < 0 {
|
||||
// A negative number has to be converted to two's-complement
|
||||
// form. So we'll subtract 1 and invert. If the
|
||||
// most-significant-bit isn't set then we'll need to pad the
|
||||
// beginning with 0xff in order to keep the number negative.
|
||||
nMinus1 := new(big.Int).Neg(n)
|
||||
nMinus1.Sub(nMinus1, bigOne)
|
||||
bytes := nMinus1.Bytes()
|
||||
for i := range bytes {
|
||||
bytes[i] ^= 0xff
|
||||
}
|
||||
if len(bytes) == 0 || bytes[0]&0x80 == 0 {
|
||||
to[0] = 0xff
|
||||
to = to[1:]
|
||||
length++
|
||||
}
|
||||
nBytes := copy(to, bytes)
|
||||
to = to[nBytes:]
|
||||
length += nBytes
|
||||
} else if n.Sign() == 0 {
|
||||
// A zero is the zero length string
|
||||
} else {
|
||||
bytes := n.Bytes()
|
||||
if len(bytes) > 0 && bytes[0]&0x80 != 0 {
|
||||
// We'll have to pad this with a 0x00 in order to
|
||||
// stop it looking like a negative number.
|
||||
to[0] = 0
|
||||
to = to[1:]
|
||||
length++
|
||||
}
|
||||
nBytes := copy(to, bytes)
|
||||
to = to[nBytes:]
|
||||
length += nBytes
|
||||
}
|
||||
|
||||
lengthBytes[0] = byte(length >> 24)
|
||||
lengthBytes[1] = byte(length >> 16)
|
||||
lengthBytes[2] = byte(length >> 8)
|
||||
lengthBytes[3] = byte(length)
|
||||
return to
|
||||
}
|
||||
|
||||
func writeInt(w io.Writer, n *big.Int) {
|
||||
length := intLength(n)
|
||||
buf := make([]byte, length)
|
||||
marshalInt(buf, n)
|
||||
w.Write(buf)
|
||||
}
|
||||
|
||||
func writeString(w io.Writer, s []byte) {
|
||||
var lengthBytes [4]byte
|
||||
lengthBytes[0] = byte(len(s) >> 24)
|
||||
lengthBytes[1] = byte(len(s) >> 16)
|
||||
lengthBytes[2] = byte(len(s) >> 8)
|
||||
lengthBytes[3] = byte(len(s))
|
||||
w.Write(lengthBytes[:])
|
||||
w.Write(s)
|
||||
}
|
||||
|
||||
func stringLength(s []byte) int {
|
||||
return 4 + len(s)
|
||||
}
|
||||
|
||||
func marshalString(to []byte, s []byte) []byte {
|
||||
to[0] = byte(len(s) >> 24)
|
||||
to[1] = byte(len(s) >> 16)
|
||||
to[2] = byte(len(s) >> 8)
|
||||
to[3] = byte(len(s))
|
||||
to = to[4:]
|
||||
copy(to, s)
|
||||
return to[len(s):]
|
||||
}
|
||||
|
||||
var bigIntType = reflect.TypeOf((*big.Int)(nil))
|
||||
|
||||
// Decode a packet into it's corresponding message.
|
||||
func decode(packet []byte) interface{} {
|
||||
var msg interface{}
|
||||
switch packet[0] {
|
||||
case msgDisconnect:
|
||||
msg = new(disconnectMsg)
|
||||
case msgServiceRequest:
|
||||
msg = new(serviceRequestMsg)
|
||||
case msgServiceAccept:
|
||||
msg = new(serviceAcceptMsg)
|
||||
case msgKexInit:
|
||||
msg = new(kexInitMsg)
|
||||
case msgKexDHInit:
|
||||
msg = new(kexDHInitMsg)
|
||||
case msgKexDHReply:
|
||||
msg = new(kexDHReplyMsg)
|
||||
case msgUserAuthRequest:
|
||||
msg = new(userAuthRequestMsg)
|
||||
case msgUserAuthFailure:
|
||||
msg = new(userAuthFailureMsg)
|
||||
case msgUserAuthPubKeyOk:
|
||||
msg = new(userAuthPubKeyOkMsg)
|
||||
case msgGlobalRequest:
|
||||
msg = new(globalRequestMsg)
|
||||
case msgRequestSuccess:
|
||||
msg = new(channelRequestSuccessMsg)
|
||||
case msgRequestFailure:
|
||||
msg = new(channelRequestFailureMsg)
|
||||
case msgChannelOpen:
|
||||
msg = new(channelOpenMsg)
|
||||
case msgChannelOpenConfirm:
|
||||
msg = new(channelOpenConfirmMsg)
|
||||
case msgChannelOpenFailure:
|
||||
msg = new(channelOpenFailureMsg)
|
||||
case msgChannelWindowAdjust:
|
||||
msg = new(windowAdjustMsg)
|
||||
case msgChannelEOF:
|
||||
msg = new(channelEOFMsg)
|
||||
case msgChannelClose:
|
||||
msg = new(channelCloseMsg)
|
||||
case msgChannelRequest:
|
||||
msg = new(channelRequestMsg)
|
||||
case msgChannelSuccess:
|
||||
msg = new(channelRequestSuccessMsg)
|
||||
case msgChannelFailure:
|
||||
msg = new(channelRequestFailureMsg)
|
||||
default:
|
||||
return UnexpectedMessageError{0, packet[0]}
|
||||
}
|
||||
if err := unmarshal(msg, packet, packet[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
return msg
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
)
|
||||
|
||||
var intLengthTests = []struct {
|
||||
val, length int
|
||||
}{
|
||||
{0, 4 + 0},
|
||||
{1, 4 + 1},
|
||||
{127, 4 + 1},
|
||||
{128, 4 + 2},
|
||||
{-1, 4 + 1},
|
||||
}
|
||||
|
||||
func TestIntLength(t *testing.T) {
|
||||
for _, test := range intLengthTests {
|
||||
v := new(big.Int).SetInt64(int64(test.val))
|
||||
length := intLength(v)
|
||||
if length != test.length {
|
||||
t.Errorf("For %d, got length %d but expected %d", test.val, length, test.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var messageTypes = []interface{}{
|
||||
&kexInitMsg{},
|
||||
&kexDHInitMsg{},
|
||||
&serviceRequestMsg{},
|
||||
&serviceAcceptMsg{},
|
||||
&userAuthRequestMsg{},
|
||||
&channelOpenMsg{},
|
||||
&channelOpenConfirmMsg{},
|
||||
&channelRequestMsg{},
|
||||
&channelRequestSuccessMsg{},
|
||||
}
|
||||
|
||||
func TestMarshalUnmarshal(t *testing.T) {
|
||||
rand := rand.New(rand.NewSource(0))
|
||||
for i, iface := range messageTypes {
|
||||
ty := reflect.ValueOf(iface).Type()
|
||||
|
||||
n := 100
|
||||
if testing.Short() {
|
||||
n = 5
|
||||
}
|
||||
for j := 0; j < n; j++ {
|
||||
v, ok := quick.Value(ty, rand)
|
||||
if !ok {
|
||||
t.Errorf("#%d: failed to create value", i)
|
||||
break
|
||||
}
|
||||
|
||||
m1 := v.Elem().Interface()
|
||||
m2 := iface
|
||||
|
||||
marshaled := marshal(msgIgnore, m1)
|
||||
if err := unmarshal(m2, marshaled, msgIgnore); err != nil {
|
||||
t.Errorf("#%d failed to unmarshal %#v: %s", i, m1, err)
|
||||
break
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(v.Interface(), m2) {
|
||||
t.Errorf("#%d\ngot: %#v\nwant:%#v\n%x", i, m2, m1, marshaled)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func randomBytes(out []byte, rand *rand.Rand) {
|
||||
for i := 0; i < len(out); i++ {
|
||||
out[i] = byte(rand.Int31())
|
||||
}
|
||||
}
|
||||
|
||||
func randomNameList(rand *rand.Rand) []string {
|
||||
ret := make([]string, rand.Int31()&15)
|
||||
for i := range ret {
|
||||
s := make([]byte, 1+(rand.Int31()&15))
|
||||
for j := range s {
|
||||
s[j] = 'a' + uint8(rand.Int31()&15)
|
||||
}
|
||||
ret[i] = string(s)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func randomInt(rand *rand.Rand) *big.Int {
|
||||
return new(big.Int).SetInt64(int64(int32(rand.Uint32())))
|
||||
}
|
||||
|
||||
func (*kexInitMsg) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||
ki := &kexInitMsg{}
|
||||
randomBytes(ki.Cookie[:], rand)
|
||||
ki.KexAlgos = randomNameList(rand)
|
||||
ki.ServerHostKeyAlgos = randomNameList(rand)
|
||||
ki.CiphersClientServer = randomNameList(rand)
|
||||
ki.CiphersServerClient = randomNameList(rand)
|
||||
ki.MACsClientServer = randomNameList(rand)
|
||||
ki.MACsServerClient = randomNameList(rand)
|
||||
ki.CompressionClientServer = randomNameList(rand)
|
||||
ki.CompressionServerClient = randomNameList(rand)
|
||||
ki.LanguagesClientServer = randomNameList(rand)
|
||||
ki.LanguagesServerClient = randomNameList(rand)
|
||||
if rand.Int31()&1 == 1 {
|
||||
ki.FirstKexFollows = true
|
||||
}
|
||||
return reflect.ValueOf(ki)
|
||||
}
|
||||
|
||||
func (*kexDHInitMsg) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||
dhi := &kexDHInitMsg{}
|
||||
dhi.X = randomInt(rand)
|
||||
return reflect.ValueOf(dhi)
|
||||
}
|
||||
+676
@@ -0,0 +1,676 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ServerConfig struct {
|
||||
rsa *rsa.PrivateKey
|
||||
rsaSerialized []byte
|
||||
|
||||
// Rand provides the source of entropy for key exchange. If Rand is
|
||||
// nil, the cryptographic random reader in package crypto/rand will
|
||||
// be used.
|
||||
Rand io.Reader
|
||||
|
||||
// NoClientAuth is true if clients are allowed to connect without
|
||||
// authenticating.
|
||||
NoClientAuth bool
|
||||
|
||||
// PasswordCallback, if non-nil, is called when a user attempts to
|
||||
// authenticate using a password. It may be called concurrently from
|
||||
// several goroutines.
|
||||
PasswordCallback func(user, password string) bool
|
||||
|
||||
// PublicKeyCallback, if non-nil, is called when a client attempts public
|
||||
// key authentication. It must return true iff the given public key is
|
||||
// valid for the given user.
|
||||
PublicKeyCallback func(user, algo string, pubkey []byte) bool
|
||||
|
||||
// Cryptographic-related configuration.
|
||||
Crypto CryptoConfig
|
||||
}
|
||||
|
||||
func (c *ServerConfig) rand() io.Reader {
|
||||
if c.Rand == nil {
|
||||
return rand.Reader
|
||||
}
|
||||
return c.Rand
|
||||
}
|
||||
|
||||
// SetRSAPrivateKey sets the private key for a Server. A Server must have a
|
||||
// private key configured in order to accept connections. The private key must
|
||||
// be in the form of a PEM encoded, PKCS#1, RSA private key. The file "id_rsa"
|
||||
// typically contains such a key.
|
||||
func (s *ServerConfig) SetRSAPrivateKey(pemBytes []byte) error {
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
if block == nil {
|
||||
return errors.New("ssh: no key found")
|
||||
}
|
||||
var err error
|
||||
s.rsa, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.rsaSerialized = marshalRSA(s.rsa)
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshalRSA serializes an RSA private key according to RFC 4256, section 6.6.
|
||||
func marshalRSA(priv *rsa.PrivateKey) []byte {
|
||||
e := new(big.Int).SetInt64(int64(priv.E))
|
||||
length := stringLength([]byte(hostAlgoRSA))
|
||||
length += intLength(e)
|
||||
length += intLength(priv.N)
|
||||
|
||||
ret := make([]byte, length)
|
||||
r := marshalString(ret, []byte(hostAlgoRSA))
|
||||
r = marshalInt(r, e)
|
||||
r = marshalInt(r, priv.N)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// parseRSA parses an RSA key according to RFC 4256, section 6.6.
|
||||
func parseRSA(in []byte) (pubKey *rsa.PublicKey, ok bool) {
|
||||
algo, in, ok := parseString(in)
|
||||
if !ok || string(algo) != hostAlgoRSA {
|
||||
return nil, false
|
||||
}
|
||||
bigE, in, ok := parseInt(in)
|
||||
if !ok || bigE.BitLen() > 24 {
|
||||
return nil, false
|
||||
}
|
||||
e := bigE.Int64()
|
||||
if e < 3 || e&1 == 0 {
|
||||
return nil, false
|
||||
}
|
||||
N, in, ok := parseInt(in)
|
||||
if !ok || len(in) > 0 {
|
||||
return nil, false
|
||||
}
|
||||
return &rsa.PublicKey{
|
||||
N: N,
|
||||
E: int(e),
|
||||
}, true
|
||||
}
|
||||
|
||||
func parseRSASig(in []byte) (sig []byte, ok bool) {
|
||||
algo, in, ok := parseString(in)
|
||||
if !ok || string(algo) != hostAlgoRSA {
|
||||
return nil, false
|
||||
}
|
||||
sig, in, ok = parseString(in)
|
||||
if len(in) > 0 {
|
||||
ok = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// cachedPubKey contains the results of querying whether a public key is
|
||||
// acceptable for a user. The cache only applies to a single ServerConn.
|
||||
type cachedPubKey struct {
|
||||
user, algo string
|
||||
pubKey []byte
|
||||
result bool
|
||||
}
|
||||
|
||||
const maxCachedPubKeys = 16
|
||||
|
||||
// A ServerConn represents an incomming connection.
|
||||
type ServerConn struct {
|
||||
*transport
|
||||
config *ServerConfig
|
||||
|
||||
channels map[uint32]*channel
|
||||
nextChanId uint32
|
||||
|
||||
// lock protects err and also allows Channels to serialise their writes
|
||||
// to out.
|
||||
lock sync.RWMutex
|
||||
err error
|
||||
|
||||
// cachedPubKeys contains the cache results of tests for public keys.
|
||||
// Since SSH clients will query whether a public key is acceptable
|
||||
// before attempting to authenticate with it, we end up with duplicate
|
||||
// queries for public key validity.
|
||||
cachedPubKeys []cachedPubKey
|
||||
}
|
||||
|
||||
// Server returns a new SSH server connection
|
||||
// using c as the underlying transport.
|
||||
func Server(c net.Conn, config *ServerConfig) *ServerConn {
|
||||
conn := &ServerConn{
|
||||
transport: newTransport(c, config.rand()),
|
||||
channels: make(map[uint32]*channel),
|
||||
config: config,
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
// kexDH performs Diffie-Hellman key agreement on a ServerConnection. The
|
||||
// returned values are given the same names as in RFC 4253, section 8.
|
||||
func (s *ServerConn) kexDH(group *dhGroup, hashFunc crypto.Hash, magics *handshakeMagics, hostKeyAlgo string) (H, K []byte, err error) {
|
||||
packet, err := s.readPacket()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var kexDHInit kexDHInitMsg
|
||||
if err = unmarshal(&kexDHInit, packet, msgKexDHInit); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if kexDHInit.X.Sign() == 0 || kexDHInit.X.Cmp(group.p) >= 0 {
|
||||
return nil, nil, errors.New("client DH parameter out of bounds")
|
||||
}
|
||||
|
||||
y, err := rand.Int(s.config.rand(), group.p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
Y := new(big.Int).Exp(group.g, y, group.p)
|
||||
kInt := new(big.Int).Exp(kexDHInit.X, y, group.p)
|
||||
|
||||
var serializedHostKey []byte
|
||||
switch hostKeyAlgo {
|
||||
case hostAlgoRSA:
|
||||
serializedHostKey = s.config.rsaSerialized
|
||||
default:
|
||||
return nil, nil, errors.New("internal error")
|
||||
}
|
||||
|
||||
h := hashFunc.New()
|
||||
writeString(h, magics.clientVersion)
|
||||
writeString(h, magics.serverVersion)
|
||||
writeString(h, magics.clientKexInit)
|
||||
writeString(h, magics.serverKexInit)
|
||||
writeString(h, serializedHostKey)
|
||||
writeInt(h, kexDHInit.X)
|
||||
writeInt(h, Y)
|
||||
K = make([]byte, intLength(kInt))
|
||||
marshalInt(K, kInt)
|
||||
h.Write(K)
|
||||
|
||||
H = h.Sum(nil)
|
||||
|
||||
h.Reset()
|
||||
h.Write(H)
|
||||
hh := h.Sum(nil)
|
||||
|
||||
var sig []byte
|
||||
switch hostKeyAlgo {
|
||||
case hostAlgoRSA:
|
||||
sig, err = rsa.SignPKCS1v15(s.config.rand(), s.config.rsa, hashFunc, hh)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
default:
|
||||
return nil, nil, errors.New("internal error")
|
||||
}
|
||||
|
||||
serializedSig := serializeSignature(hostAlgoRSA, sig)
|
||||
|
||||
kexDHReply := kexDHReplyMsg{
|
||||
HostKey: serializedHostKey,
|
||||
Y: Y,
|
||||
Signature: serializedSig,
|
||||
}
|
||||
packet = marshal(msgKexDHReply, kexDHReply)
|
||||
|
||||
err = s.writePacket(packet)
|
||||
return
|
||||
}
|
||||
|
||||
// serverVersion is the fixed identification string that Server will use.
|
||||
var serverVersion = []byte("SSH-2.0-Go\r\n")
|
||||
|
||||
// Handshake performs an SSH transport and client authentication on the given ServerConn.
|
||||
func (s *ServerConn) Handshake() error {
|
||||
var magics handshakeMagics
|
||||
if _, err := s.Write(serverVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
magics.serverVersion = serverVersion[:len(serverVersion)-2]
|
||||
|
||||
version, err := readVersion(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
magics.clientVersion = version
|
||||
|
||||
serverKexInit := kexInitMsg{
|
||||
KexAlgos: supportedKexAlgos,
|
||||
ServerHostKeyAlgos: supportedHostKeyAlgos,
|
||||
CiphersClientServer: s.config.Crypto.ciphers(),
|
||||
CiphersServerClient: s.config.Crypto.ciphers(),
|
||||
MACsClientServer: supportedMACs,
|
||||
MACsServerClient: supportedMACs,
|
||||
CompressionClientServer: supportedCompressions,
|
||||
CompressionServerClient: supportedCompressions,
|
||||
}
|
||||
kexInitPacket := marshal(msgKexInit, serverKexInit)
|
||||
magics.serverKexInit = kexInitPacket
|
||||
|
||||
if err := s.writePacket(kexInitPacket); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
packet, err := s.readPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
magics.clientKexInit = packet
|
||||
|
||||
var clientKexInit kexInitMsg
|
||||
if err = unmarshal(&clientKexInit, packet, msgKexInit); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kexAlgo, hostKeyAlgo, ok := findAgreedAlgorithms(s.transport, &clientKexInit, &serverKexInit)
|
||||
if !ok {
|
||||
return errors.New("ssh: no common algorithms")
|
||||
}
|
||||
|
||||
if clientKexInit.FirstKexFollows && kexAlgo != clientKexInit.KexAlgos[0] {
|
||||
// The client sent a Kex message for the wrong algorithm,
|
||||
// which we have to ignore.
|
||||
if _, err := s.readPacket(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var H, K []byte
|
||||
var hashFunc crypto.Hash
|
||||
switch kexAlgo {
|
||||
case kexAlgoDH14SHA1:
|
||||
hashFunc = crypto.SHA1
|
||||
dhGroup14Once.Do(initDHGroup14)
|
||||
H, K, err = s.kexDH(dhGroup14, hashFunc, &magics, hostKeyAlgo)
|
||||
default:
|
||||
err = errors.New("ssh: unexpected key exchange algorithm " + kexAlgo)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = s.writePacket([]byte{msgNewKeys}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = s.transport.writer.setupKeys(serverKeys, K, H, H, hashFunc); err != nil {
|
||||
return err
|
||||
}
|
||||
if packet, err = s.readPacket(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if packet[0] != msgNewKeys {
|
||||
return UnexpectedMessageError{msgNewKeys, packet[0]}
|
||||
}
|
||||
if err = s.transport.reader.setupKeys(clientKeys, K, H, H, hashFunc); err != nil {
|
||||
return err
|
||||
}
|
||||
if packet, err = s.readPacket(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var serviceRequest serviceRequestMsg
|
||||
if err = unmarshal(&serviceRequest, packet, msgServiceRequest); err != nil {
|
||||
return err
|
||||
}
|
||||
if serviceRequest.Service != serviceUserAuth {
|
||||
return errors.New("ssh: requested service '" + serviceRequest.Service + "' before authenticating")
|
||||
}
|
||||
serviceAccept := serviceAcceptMsg{
|
||||
Service: serviceUserAuth,
|
||||
}
|
||||
if err = s.writePacket(marshal(msgServiceAccept, serviceAccept)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = s.authenticate(H); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isAcceptableAlgo(algo string) bool {
|
||||
return algo == hostAlgoRSA
|
||||
}
|
||||
|
||||
// testPubKey returns true if the given public key is acceptable for the user.
|
||||
func (s *ServerConn) testPubKey(user, algo string, pubKey []byte) bool {
|
||||
if s.config.PublicKeyCallback == nil || !isAcceptableAlgo(algo) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, c := range s.cachedPubKeys {
|
||||
if c.user == user && c.algo == algo && bytes.Equal(c.pubKey, pubKey) {
|
||||
return c.result
|
||||
}
|
||||
}
|
||||
|
||||
result := s.config.PublicKeyCallback(user, algo, pubKey)
|
||||
if len(s.cachedPubKeys) < maxCachedPubKeys {
|
||||
c := cachedPubKey{
|
||||
user: user,
|
||||
algo: algo,
|
||||
pubKey: make([]byte, len(pubKey)),
|
||||
result: result,
|
||||
}
|
||||
copy(c.pubKey, pubKey)
|
||||
s.cachedPubKeys = append(s.cachedPubKeys, c)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *ServerConn) authenticate(H []byte) error {
|
||||
var userAuthReq userAuthRequestMsg
|
||||
var err error
|
||||
var packet []byte
|
||||
|
||||
userAuthLoop:
|
||||
for {
|
||||
if packet, err = s.readPacket(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = unmarshal(&userAuthReq, packet, msgUserAuthRequest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if userAuthReq.Service != serviceSSH {
|
||||
return errors.New("ssh: client attempted to negotiate for unknown service: " + userAuthReq.Service)
|
||||
}
|
||||
|
||||
switch userAuthReq.Method {
|
||||
case "none":
|
||||
if s.config.NoClientAuth {
|
||||
break userAuthLoop
|
||||
}
|
||||
case "password":
|
||||
if s.config.PasswordCallback == nil {
|
||||
break
|
||||
}
|
||||
payload := userAuthReq.Payload
|
||||
if len(payload) < 1 || payload[0] != 0 {
|
||||
return ParseError{msgUserAuthRequest}
|
||||
}
|
||||
payload = payload[1:]
|
||||
password, payload, ok := parseString(payload)
|
||||
if !ok || len(payload) > 0 {
|
||||
return ParseError{msgUserAuthRequest}
|
||||
}
|
||||
|
||||
if s.config.PasswordCallback(userAuthReq.User, string(password)) {
|
||||
break userAuthLoop
|
||||
}
|
||||
case "publickey":
|
||||
if s.config.PublicKeyCallback == nil {
|
||||
break
|
||||
}
|
||||
payload := userAuthReq.Payload
|
||||
if len(payload) < 1 {
|
||||
return ParseError{msgUserAuthRequest}
|
||||
}
|
||||
isQuery := payload[0] == 0
|
||||
payload = payload[1:]
|
||||
algoBytes, payload, ok := parseString(payload)
|
||||
if !ok {
|
||||
return ParseError{msgUserAuthRequest}
|
||||
}
|
||||
algo := string(algoBytes)
|
||||
|
||||
pubKey, payload, ok := parseString(payload)
|
||||
if !ok {
|
||||
return ParseError{msgUserAuthRequest}
|
||||
}
|
||||
if isQuery {
|
||||
// The client can query if the given public key
|
||||
// would be ok.
|
||||
if len(payload) > 0 {
|
||||
return ParseError{msgUserAuthRequest}
|
||||
}
|
||||
if s.testPubKey(userAuthReq.User, algo, pubKey) {
|
||||
okMsg := userAuthPubKeyOkMsg{
|
||||
Algo: algo,
|
||||
PubKey: string(pubKey),
|
||||
}
|
||||
if err = s.writePacket(marshal(msgUserAuthPubKeyOk, okMsg)); err != nil {
|
||||
return err
|
||||
}
|
||||
continue userAuthLoop
|
||||
}
|
||||
} else {
|
||||
sig, payload, ok := parseString(payload)
|
||||
if !ok || len(payload) > 0 {
|
||||
return ParseError{msgUserAuthRequest}
|
||||
}
|
||||
if !isAcceptableAlgo(algo) {
|
||||
break
|
||||
}
|
||||
rsaSig, ok := parseRSASig(sig)
|
||||
if !ok {
|
||||
return ParseError{msgUserAuthRequest}
|
||||
}
|
||||
signedData := buildDataSignedForAuth(H, userAuthReq, algoBytes, pubKey)
|
||||
switch algo {
|
||||
case hostAlgoRSA:
|
||||
hashFunc := crypto.SHA1
|
||||
h := hashFunc.New()
|
||||
h.Write(signedData)
|
||||
digest := h.Sum(nil)
|
||||
rsaKey, ok := parseRSA(pubKey)
|
||||
if !ok {
|
||||
return ParseError{msgUserAuthRequest}
|
||||
}
|
||||
if rsa.VerifyPKCS1v15(rsaKey, hashFunc, digest, rsaSig) != nil {
|
||||
return ParseError{msgUserAuthRequest}
|
||||
}
|
||||
default:
|
||||
return errors.New("ssh: isAcceptableAlgo incorrect")
|
||||
}
|
||||
if s.testPubKey(userAuthReq.User, algo, pubKey) {
|
||||
break userAuthLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var failureMsg userAuthFailureMsg
|
||||
if s.config.PasswordCallback != nil {
|
||||
failureMsg.Methods = append(failureMsg.Methods, "password")
|
||||
}
|
||||
if s.config.PublicKeyCallback != nil {
|
||||
failureMsg.Methods = append(failureMsg.Methods, "publickey")
|
||||
}
|
||||
|
||||
if len(failureMsg.Methods) == 0 {
|
||||
return errors.New("ssh: no authentication methods configured but NoClientAuth is also false")
|
||||
}
|
||||
|
||||
if err = s.writePacket(marshal(msgUserAuthFailure, failureMsg)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
packet = []byte{msgUserAuthSuccess}
|
||||
if err = s.writePacket(packet); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const defaultWindowSize = 32768
|
||||
|
||||
// Accept reads and processes messages on a ServerConn. It must be called
|
||||
// in order to demultiplex messages to any resulting Channels.
|
||||
func (s *ServerConn) Accept() (Channel, error) {
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
|
||||
for {
|
||||
packet, err := s.readPacket()
|
||||
if err != nil {
|
||||
|
||||
s.lock.Lock()
|
||||
s.err = err
|
||||
s.lock.Unlock()
|
||||
|
||||
for _, c := range s.channels {
|
||||
c.dead = true
|
||||
c.handleData(nil)
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch packet[0] {
|
||||
case msgChannelData:
|
||||
if len(packet) < 9 {
|
||||
// malformed data packet
|
||||
return nil, ParseError{msgChannelData}
|
||||
}
|
||||
peersId := uint32(packet[1])<<24 | uint32(packet[2])<<16 | uint32(packet[3])<<8 | uint32(packet[4])
|
||||
s.lock.Lock()
|
||||
c, ok := s.channels[peersId]
|
||||
if !ok {
|
||||
s.lock.Unlock()
|
||||
continue
|
||||
}
|
||||
if length := int(packet[5])<<24 | int(packet[6])<<16 | int(packet[7])<<8 | int(packet[8]); length > 0 {
|
||||
packet = packet[9:]
|
||||
c.handleData(packet[:length])
|
||||
}
|
||||
s.lock.Unlock()
|
||||
default:
|
||||
switch msg := decode(packet).(type) {
|
||||
case *channelOpenMsg:
|
||||
c := new(channel)
|
||||
c.chanType = msg.ChanType
|
||||
c.theirId = msg.PeersId
|
||||
c.theirWindow = msg.PeersWindow
|
||||
c.maxPacketSize = msg.MaxPacketSize
|
||||
c.extraData = msg.TypeSpecificData
|
||||
c.myWindow = defaultWindowSize
|
||||
c.serverConn = s
|
||||
c.cond = sync.NewCond(&c.lock)
|
||||
c.pendingData = make([]byte, c.myWindow)
|
||||
|
||||
s.lock.Lock()
|
||||
c.myId = s.nextChanId
|
||||
s.nextChanId++
|
||||
s.channels[c.myId] = c
|
||||
s.lock.Unlock()
|
||||
return c, nil
|
||||
|
||||
case *channelRequestMsg:
|
||||
s.lock.Lock()
|
||||
c, ok := s.channels[msg.PeersId]
|
||||
if !ok {
|
||||
s.lock.Unlock()
|
||||
continue
|
||||
}
|
||||
c.handlePacket(msg)
|
||||
s.lock.Unlock()
|
||||
|
||||
case *channelEOFMsg:
|
||||
s.lock.Lock()
|
||||
c, ok := s.channels[msg.PeersId]
|
||||
if !ok {
|
||||
s.lock.Unlock()
|
||||
continue
|
||||
}
|
||||
c.handlePacket(msg)
|
||||
s.lock.Unlock()
|
||||
|
||||
case *channelCloseMsg:
|
||||
s.lock.Lock()
|
||||
c, ok := s.channels[msg.PeersId]
|
||||
if !ok {
|
||||
s.lock.Unlock()
|
||||
continue
|
||||
}
|
||||
c.handlePacket(msg)
|
||||
s.lock.Unlock()
|
||||
|
||||
case *globalRequestMsg:
|
||||
if msg.WantReply {
|
||||
if err := s.writePacket([]byte{msgRequestFailure}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
case UnexpectedMessageError:
|
||||
return nil, msg
|
||||
case *disconnectMsg:
|
||||
return nil, io.EOF
|
||||
default:
|
||||
// Unknown message. Ignore.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// A Listener implements a network listener (net.Listener) for SSH connections.
|
||||
type Listener struct {
|
||||
listener net.Listener
|
||||
config *ServerConfig
|
||||
}
|
||||
|
||||
// Accept waits for and returns the next incoming SSH connection.
|
||||
// The receiver should call Handshake() in another goroutine
|
||||
// to avoid blocking the accepter.
|
||||
func (l *Listener) Accept() (*ServerConn, error) {
|
||||
c, err := l.listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn := Server(c, l.config)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Addr returns the listener's network address.
|
||||
func (l *Listener) Addr() net.Addr {
|
||||
return l.listener.Addr()
|
||||
}
|
||||
|
||||
// Close closes the listener.
|
||||
func (l *Listener) Close() error {
|
||||
return l.listener.Close()
|
||||
}
|
||||
|
||||
// Listen creates an SSH listener accepting connections on
|
||||
// the given network address using net.Listen.
|
||||
func Listen(network, addr string, config *ServerConfig) (*Listener, error) {
|
||||
l, err := net.Listen(network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Listener{
|
||||
l,
|
||||
config,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
// A Terminal is capable of parsing and generating virtual terminal
|
||||
// data from an SSH client.
|
||||
type Terminal interface {
|
||||
ReadLine() (line string, err error)
|
||||
SetSize(x, y int)
|
||||
Write([]byte) (int, error)
|
||||
}
|
||||
|
||||
// ServerTerminal contains the state for running a terminal that is capable of
|
||||
// reading lines of input.
|
||||
type ServerTerminal struct {
|
||||
Term Terminal
|
||||
Channel Channel
|
||||
}
|
||||
|
||||
// parsePtyRequest parses the payload of the pty-req message and extracts the
|
||||
// dimensions of the terminal. See RFC 4254, section 6.2.
|
||||
func parsePtyRequest(s []byte) (width, height int, ok bool) {
|
||||
_, s, ok = parseString(s)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
width32, s, ok := parseUint32(s)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
height32, _, ok := parseUint32(s)
|
||||
width = int(width32)
|
||||
height = int(height32)
|
||||
if width < 1 {
|
||||
ok = false
|
||||
}
|
||||
if height < 1 {
|
||||
ok = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ss *ServerTerminal) Write(buf []byte) (n int, err error) {
|
||||
return ss.Term.Write(buf)
|
||||
}
|
||||
|
||||
// ReadLine returns a line of input from the terminal.
|
||||
func (ss *ServerTerminal) ReadLine() (line string, err error) {
|
||||
for {
|
||||
if line, err = ss.Term.ReadLine(); err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
req, ok := err.(ChannelRequest)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ok = false
|
||||
switch req.Request {
|
||||
case "pty-req":
|
||||
var width, height int
|
||||
width, height, ok = parsePtyRequest(req.Payload)
|
||||
ss.Term.SetSize(width, height)
|
||||
case "shell":
|
||||
ok = true
|
||||
if len(req.Payload) > 0 {
|
||||
// We don't accept any commands, only the default shell.
|
||||
ok = false
|
||||
}
|
||||
case "env":
|
||||
ok = true
|
||||
}
|
||||
if req.WantReply {
|
||||
ss.Channel.AckRequest(ok)
|
||||
}
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
+494
@@ -0,0 +1,494 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
// Session implements an interactive session described in
|
||||
// "RFC 4254, section 6".
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type Signal string
|
||||
|
||||
// POSIX signals as listed in RFC 4254 Section 6.10.
|
||||
const (
|
||||
SIGABRT Signal = "ABRT"
|
||||
SIGALRM Signal = "ALRM"
|
||||
SIGFPE Signal = "FPE"
|
||||
SIGHUP Signal = "HUP"
|
||||
SIGILL Signal = "ILL"
|
||||
SIGINT Signal = "INT"
|
||||
SIGKILL Signal = "KILL"
|
||||
SIGPIPE Signal = "PIPE"
|
||||
SIGQUIT Signal = "QUIT"
|
||||
SIGSEGV Signal = "SEGV"
|
||||
SIGTERM Signal = "TERM"
|
||||
SIGUSR1 Signal = "USR1"
|
||||
SIGUSR2 Signal = "USR2"
|
||||
)
|
||||
|
||||
var signals = map[Signal]int{
|
||||
SIGABRT: 6,
|
||||
SIGALRM: 14,
|
||||
SIGFPE: 8,
|
||||
SIGHUP: 1,
|
||||
SIGILL: 4,
|
||||
SIGINT: 2,
|
||||
SIGKILL: 9,
|
||||
SIGPIPE: 13,
|
||||
SIGQUIT: 3,
|
||||
SIGSEGV: 11,
|
||||
SIGTERM: 15,
|
||||
}
|
||||
|
||||
// A Session represents a connection to a remote command or shell.
|
||||
type Session struct {
|
||||
// Stdin specifies the remote process's standard input.
|
||||
// If Stdin is nil, the remote process reads from an empty
|
||||
// bytes.Buffer.
|
||||
Stdin io.Reader
|
||||
|
||||
// Stdout and Stderr specify the remote process's standard
|
||||
// output and error.
|
||||
//
|
||||
// If either is nil, Run connects the corresponding file
|
||||
// descriptor to an instance of ioutil.Discard. There is a
|
||||
// fixed amount of buffering that is shared for the two streams.
|
||||
// If either blocks it may eventually cause the remote
|
||||
// command to block.
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
|
||||
*clientChan // the channel backing this session
|
||||
|
||||
started bool // true once Start, Run or Shell is invoked.
|
||||
copyFuncs []func() error
|
||||
errors chan error // one send per copyFunc
|
||||
|
||||
// true if pipe method is active
|
||||
stdinpipe, stdoutpipe, stderrpipe bool
|
||||
}
|
||||
|
||||
// RFC 4254 Section 6.4.
|
||||
type setenvRequest struct {
|
||||
PeersId uint32
|
||||
Request string
|
||||
WantReply bool
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// Setenv sets an environment variable that will be applied to any
|
||||
// command executed by Shell or Run.
|
||||
func (s *Session) Setenv(name, value string) error {
|
||||
req := setenvRequest{
|
||||
PeersId: s.peersId,
|
||||
Request: "env",
|
||||
WantReply: true,
|
||||
Name: name,
|
||||
Value: value,
|
||||
}
|
||||
if err := s.writePacket(marshal(msgChannelRequest, req)); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.waitForResponse()
|
||||
}
|
||||
|
||||
// An empty mode list, see RFC 4254 Section 8.
|
||||
var emptyModelist = "\x00"
|
||||
|
||||
// RFC 4254 Section 6.2.
|
||||
type ptyRequestMsg struct {
|
||||
PeersId uint32
|
||||
Request string
|
||||
WantReply bool
|
||||
Term string
|
||||
Columns uint32
|
||||
Rows uint32
|
||||
Width uint32
|
||||
Height uint32
|
||||
Modelist string
|
||||
}
|
||||
|
||||
// RequestPty requests the association of a pty with the session on the remote host.
|
||||
func (s *Session) RequestPty(term string, h, w int) error {
|
||||
req := ptyRequestMsg{
|
||||
PeersId: s.peersId,
|
||||
Request: "pty-req",
|
||||
WantReply: true,
|
||||
Term: term,
|
||||
Columns: uint32(w),
|
||||
Rows: uint32(h),
|
||||
Width: uint32(w * 8),
|
||||
Height: uint32(h * 8),
|
||||
Modelist: emptyModelist,
|
||||
}
|
||||
if err := s.writePacket(marshal(msgChannelRequest, req)); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.waitForResponse()
|
||||
}
|
||||
|
||||
// RFC 4254 Section 6.9.
|
||||
type signalMsg struct {
|
||||
PeersId uint32
|
||||
Request string
|
||||
WantReply bool
|
||||
Signal string
|
||||
}
|
||||
|
||||
// Signal sends the given signal to the remote process.
|
||||
// sig is one of the SIG* constants.
|
||||
func (s *Session) Signal(sig Signal) error {
|
||||
req := signalMsg{
|
||||
PeersId: s.peersId,
|
||||
Request: "signal",
|
||||
WantReply: false,
|
||||
Signal: string(sig),
|
||||
}
|
||||
return s.writePacket(marshal(msgChannelRequest, req))
|
||||
}
|
||||
|
||||
// RFC 4254 Section 6.5.
|
||||
type execMsg struct {
|
||||
PeersId uint32
|
||||
Request string
|
||||
WantReply bool
|
||||
Command string
|
||||
}
|
||||
|
||||
// Start runs cmd on the remote host. Typically, the remote
|
||||
// server passes cmd to the shell for interpretation.
|
||||
// A Session only accepts one call to Run, Start or Shell.
|
||||
func (s *Session) Start(cmd string) error {
|
||||
if s.started {
|
||||
return errors.New("ssh: session already started")
|
||||
}
|
||||
req := execMsg{
|
||||
PeersId: s.peersId,
|
||||
Request: "exec",
|
||||
WantReply: true,
|
||||
Command: cmd,
|
||||
}
|
||||
if err := s.writePacket(marshal(msgChannelRequest, req)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.waitForResponse(); err != nil {
|
||||
return fmt.Errorf("ssh: could not execute command %s: %v", cmd, err)
|
||||
}
|
||||
return s.start()
|
||||
}
|
||||
|
||||
// Run runs cmd on the remote host. Typically, the remote
|
||||
// server passes cmd to the shell for interpretation.
|
||||
// A Session only accepts one call to Run, Start or Shell.
|
||||
//
|
||||
// The returned error is nil if the command runs, has no problems
|
||||
// copying stdin, stdout, and stderr, and exits with a zero exit
|
||||
// status.
|
||||
//
|
||||
// If the command fails to run or doesn't complete successfully, the
|
||||
// error is of type *ExitError. Other error types may be
|
||||
// returned for I/O problems.
|
||||
func (s *Session) Run(cmd string) error {
|
||||
err := s.Start(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Wait()
|
||||
}
|
||||
|
||||
// Shell starts a login shell on the remote host. A Session only
|
||||
// accepts one call to Run, Start or Shell.
|
||||
func (s *Session) Shell() error {
|
||||
if s.started {
|
||||
return errors.New("ssh: session already started")
|
||||
}
|
||||
req := channelRequestMsg{
|
||||
PeersId: s.peersId,
|
||||
Request: "shell",
|
||||
WantReply: true,
|
||||
}
|
||||
if err := s.writePacket(marshal(msgChannelRequest, req)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.waitForResponse(); err != nil {
|
||||
return fmt.Errorf("ssh: cound not execute shell: %v", err)
|
||||
}
|
||||
return s.start()
|
||||
}
|
||||
|
||||
func (s *Session) waitForResponse() error {
|
||||
msg := <-s.msg
|
||||
switch msg.(type) {
|
||||
case *channelRequestSuccessMsg:
|
||||
return nil
|
||||
case *channelRequestFailureMsg:
|
||||
return errors.New("request failed")
|
||||
}
|
||||
return fmt.Errorf("unknown packet %T received: %v", msg, msg)
|
||||
}
|
||||
|
||||
func (s *Session) start() error {
|
||||
s.started = true
|
||||
|
||||
type F func(*Session)
|
||||
for _, setupFd := range []F{(*Session).stdin, (*Session).stdout, (*Session).stderr} {
|
||||
setupFd(s)
|
||||
}
|
||||
|
||||
s.errors = make(chan error, len(s.copyFuncs))
|
||||
for _, fn := range s.copyFuncs {
|
||||
go func(fn func() error) {
|
||||
s.errors <- fn()
|
||||
}(fn)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait waits for the remote command to exit.
|
||||
//
|
||||
// The returned error is nil if the command runs, has no problems
|
||||
// copying stdin, stdout, and stderr, and exits with a zero exit
|
||||
// status.
|
||||
//
|
||||
// If the command fails to run or doesn't complete successfully, the
|
||||
// error is of type *ExitError. Other error types may be
|
||||
// returned for I/O problems.
|
||||
func (s *Session) Wait() error {
|
||||
if !s.started {
|
||||
return errors.New("ssh: session not started")
|
||||
}
|
||||
waitErr := s.wait()
|
||||
|
||||
var copyError error
|
||||
for _ = range s.copyFuncs {
|
||||
if err := <-s.errors; err != nil && copyError == nil {
|
||||
copyError = err
|
||||
}
|
||||
}
|
||||
if waitErr != nil {
|
||||
return waitErr
|
||||
}
|
||||
return copyError
|
||||
}
|
||||
|
||||
func (s *Session) wait() error {
|
||||
wm := Waitmsg{status: -1}
|
||||
|
||||
// Wait for msg channel to be closed before returning.
|
||||
for msg := range s.msg {
|
||||
switch msg := msg.(type) {
|
||||
case *channelRequestMsg:
|
||||
switch msg.Request {
|
||||
case "exit-status":
|
||||
d := msg.RequestSpecificData
|
||||
wm.status = int(d[0])<<24 | int(d[1])<<16 | int(d[2])<<8 | int(d[3])
|
||||
case "exit-signal":
|
||||
signal, rest, ok := parseString(msg.RequestSpecificData)
|
||||
if !ok {
|
||||
return fmt.Errorf("wait: could not parse request data: %v", msg.RequestSpecificData)
|
||||
}
|
||||
wm.signal = safeString(string(signal))
|
||||
|
||||
// skip coreDumped bool
|
||||
if len(rest) == 0 {
|
||||
return fmt.Errorf("wait: could not parse request data: %v", msg.RequestSpecificData)
|
||||
}
|
||||
rest = rest[1:]
|
||||
|
||||
errmsg, rest, ok := parseString(rest)
|
||||
if !ok {
|
||||
return fmt.Errorf("wait: could not parse request data: %v", msg.RequestSpecificData)
|
||||
}
|
||||
wm.msg = safeString(string(errmsg))
|
||||
|
||||
lang, _, ok := parseString(rest)
|
||||
if !ok {
|
||||
return fmt.Errorf("wait: could not parse request data: %v", msg.RequestSpecificData)
|
||||
}
|
||||
wm.lang = safeString(string(lang))
|
||||
default:
|
||||
return fmt.Errorf("wait: unexpected channel request: %v", msg)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("wait: unexpected packet %T received: %v", msg, msg)
|
||||
}
|
||||
}
|
||||
if wm.status == 0 {
|
||||
return nil
|
||||
}
|
||||
if wm.status == -1 {
|
||||
// exit-status was never sent from server
|
||||
if wm.signal == "" {
|
||||
return errors.New("wait: remote command exited without exit status or exit signal")
|
||||
}
|
||||
wm.status = 128
|
||||
if _, ok := signals[Signal(wm.signal)]; ok {
|
||||
wm.status += signals[Signal(wm.signal)]
|
||||
}
|
||||
}
|
||||
return &ExitError{wm}
|
||||
}
|
||||
|
||||
func (s *Session) stdin() {
|
||||
if s.stdinpipe {
|
||||
return
|
||||
}
|
||||
if s.Stdin == nil {
|
||||
s.Stdin = new(bytes.Buffer)
|
||||
}
|
||||
s.copyFuncs = append(s.copyFuncs, func() error {
|
||||
_, err := io.Copy(s.clientChan.stdin, s.Stdin)
|
||||
if err1 := s.clientChan.stdin.Close(); err == nil {
|
||||
err = err1
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Session) stdout() {
|
||||
if s.stdoutpipe {
|
||||
return
|
||||
}
|
||||
if s.Stdout == nil {
|
||||
s.Stdout = ioutil.Discard
|
||||
}
|
||||
s.copyFuncs = append(s.copyFuncs, func() error {
|
||||
_, err := io.Copy(s.Stdout, s.clientChan.stdout)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Session) stderr() {
|
||||
if s.stderrpipe {
|
||||
return
|
||||
}
|
||||
if s.Stderr == nil {
|
||||
s.Stderr = ioutil.Discard
|
||||
}
|
||||
s.copyFuncs = append(s.copyFuncs, func() error {
|
||||
_, err := io.Copy(s.Stderr, s.clientChan.stderr)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// StdinPipe returns a pipe that will be connected to the
|
||||
// remote command's standard input when the command starts.
|
||||
func (s *Session) StdinPipe() (io.WriteCloser, error) {
|
||||
if s.Stdin != nil {
|
||||
return nil, errors.New("ssh: Stdin already set")
|
||||
}
|
||||
if s.started {
|
||||
return nil, errors.New("ssh: StdinPipe after process started")
|
||||
}
|
||||
s.stdinpipe = true
|
||||
return s.clientChan.stdin, nil
|
||||
}
|
||||
|
||||
// StdoutPipe returns a pipe that will be connected to the
|
||||
// remote command's standard output when the command starts.
|
||||
// There is a fixed amount of buffering that is shared between
|
||||
// stdout and stderr streams. If the StdoutPipe reader is
|
||||
// not serviced fast enought it may eventually cause the
|
||||
// remote command to block.
|
||||
func (s *Session) StdoutPipe() (io.Reader, error) {
|
||||
if s.Stdout != nil {
|
||||
return nil, errors.New("ssh: Stdout already set")
|
||||
}
|
||||
if s.started {
|
||||
return nil, errors.New("ssh: StdoutPipe after process started")
|
||||
}
|
||||
s.stdoutpipe = true
|
||||
return s.clientChan.stdout, nil
|
||||
}
|
||||
|
||||
// StderrPipe returns a pipe that will be connected to the
|
||||
// remote command's standard error when the command starts.
|
||||
// There is a fixed amount of buffering that is shared between
|
||||
// stdout and stderr streams. If the StderrPipe reader is
|
||||
// not serviced fast enought it may eventually cause the
|
||||
// remote command to block.
|
||||
func (s *Session) StderrPipe() (io.Reader, error) {
|
||||
if s.Stderr != nil {
|
||||
return nil, errors.New("ssh: Stderr already set")
|
||||
}
|
||||
if s.started {
|
||||
return nil, errors.New("ssh: StderrPipe after process started")
|
||||
}
|
||||
s.stderrpipe = true
|
||||
return s.clientChan.stderr, nil
|
||||
}
|
||||
|
||||
// TODO(dfc) add Output and CombinedOutput helpers
|
||||
|
||||
// NewSession returns a new interactive session on the remote host.
|
||||
func (c *ClientConn) NewSession() (*Session, error) {
|
||||
ch := c.newChan(c.transport)
|
||||
if err := c.writePacket(marshal(msgChannelOpen, channelOpenMsg{
|
||||
ChanType: "session",
|
||||
PeersId: ch.id,
|
||||
PeersWindow: 1 << 14,
|
||||
MaxPacketSize: 1 << 15, // RFC 4253 6.1
|
||||
})); err != nil {
|
||||
c.chanlist.remove(ch.id)
|
||||
return nil, err
|
||||
}
|
||||
if err := ch.waitForChannelOpenResponse(); err != nil {
|
||||
c.chanlist.remove(ch.id)
|
||||
return nil, fmt.Errorf("ssh: unable to open session: %v", err)
|
||||
}
|
||||
return &Session{
|
||||
clientChan: ch,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// An ExitError reports unsuccessful completion of a remote command.
|
||||
type ExitError struct {
|
||||
Waitmsg
|
||||
}
|
||||
|
||||
func (e *ExitError) Error() string {
|
||||
return e.Waitmsg.String()
|
||||
}
|
||||
|
||||
// Waitmsg stores the information about an exited remote command
|
||||
// as reported by Wait.
|
||||
type Waitmsg struct {
|
||||
status int
|
||||
signal string
|
||||
msg string
|
||||
lang string
|
||||
}
|
||||
|
||||
// ExitStatus returns the exit status of the remote command.
|
||||
func (w Waitmsg) ExitStatus() int {
|
||||
return w.status
|
||||
}
|
||||
|
||||
// Signal returns the exit signal of the remote command if
|
||||
// it was terminated violently.
|
||||
func (w Waitmsg) Signal() string {
|
||||
return w.signal
|
||||
}
|
||||
|
||||
// Msg returns the exit message given by the remote command
|
||||
func (w Waitmsg) Msg() string {
|
||||
return w.msg
|
||||
}
|
||||
|
||||
// Lang returns the language tag. See RFC 3066
|
||||
func (w Waitmsg) Lang() string {
|
||||
return w.lang
|
||||
}
|
||||
|
||||
func (w Waitmsg) String() string {
|
||||
return fmt.Sprintf("Process exited with: %v. Reason was: %v (%v)", w.status, w.msg, w.signal)
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
// Session tests.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"exp/terminal"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type serverType func(*channel)
|
||||
|
||||
// dial constructs a new test server and returns a *ClientConn.
|
||||
func dial(handler serverType, t *testing.T) *ClientConn {
|
||||
pw := password("tiger")
|
||||
serverConfig.PasswordCallback = func(user, pass string) bool {
|
||||
return user == "testuser" && pass == string(pw)
|
||||
}
|
||||
serverConfig.PublicKeyCallback = nil
|
||||
|
||||
l, err := Listen("tcp", "127.0.0.1:0", serverConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to listen: %s", err)
|
||||
}
|
||||
go func() {
|
||||
defer l.Close()
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("Unable to accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
if err := conn.Handshake(); err != nil {
|
||||
t.Errorf("Unable to handshake: %v", err)
|
||||
return
|
||||
}
|
||||
for {
|
||||
ch, err := conn.Accept()
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Unable to accept incoming channel request: %v", err)
|
||||
return
|
||||
}
|
||||
if ch.ChannelType() != "session" {
|
||||
ch.Reject(UnknownChannelType, "unknown channel type")
|
||||
continue
|
||||
}
|
||||
ch.Accept()
|
||||
go handler(ch.(*channel))
|
||||
}
|
||||
t.Log("done")
|
||||
}()
|
||||
|
||||
config := &ClientConfig{
|
||||
User: "testuser",
|
||||
Auth: []ClientAuth{
|
||||
ClientAuthPassword(pw),
|
||||
},
|
||||
}
|
||||
|
||||
c, err := Dial("tcp", l.Addr().String(), config)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to dial remote side: %s", err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Test a simple string is returned to session.Stdout.
|
||||
func TestSessionShell(t *testing.T) {
|
||||
conn := dial(shellHandler, t)
|
||||
defer conn.Close()
|
||||
session, err := conn.NewSession()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to request new session: %s", err)
|
||||
}
|
||||
defer session.Close()
|
||||
stdout := new(bytes.Buffer)
|
||||
session.Stdout = stdout
|
||||
if err := session.Shell(); err != nil {
|
||||
t.Fatalf("Unable to execute command: %s", err)
|
||||
}
|
||||
if err := session.Wait(); err != nil {
|
||||
t.Fatalf("Remote command did not exit cleanly: %s", err)
|
||||
}
|
||||
actual := stdout.String()
|
||||
if actual != "golang" {
|
||||
t.Fatalf("Remote shell did not return expected string: expected=golang, actual=%s", actual)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(dfc) add support for Std{in,err}Pipe when the Server supports it.
|
||||
|
||||
// Test a simple string is returned via StdoutPipe.
|
||||
func TestSessionStdoutPipe(t *testing.T) {
|
||||
conn := dial(shellHandler, t)
|
||||
defer conn.Close()
|
||||
session, err := conn.NewSession()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to request new session: %s", err)
|
||||
}
|
||||
defer session.Close()
|
||||
stdout, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to request StdoutPipe(): %v", err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := session.Shell(); err != nil {
|
||||
t.Fatalf("Unable to execute command: %s", err)
|
||||
}
|
||||
done := make(chan bool, 1)
|
||||
go func() {
|
||||
if _, err := io.Copy(&buf, stdout); err != nil {
|
||||
t.Errorf("Copy of stdout failed: %v", err)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
if err := session.Wait(); err != nil {
|
||||
t.Fatalf("Remote command did not exit cleanly: %s", err)
|
||||
}
|
||||
<-done
|
||||
actual := buf.String()
|
||||
if actual != "golang" {
|
||||
t.Fatalf("Remote shell did not return expected string: expected=golang, actual=%s", actual)
|
||||
}
|
||||
}
|
||||
|
||||
// Test non-0 exit status is returned correctly.
|
||||
func TestExitStatusNonZero(t *testing.T) {
|
||||
conn := dial(exitStatusNonZeroHandler, t)
|
||||
defer conn.Close()
|
||||
session, err := conn.NewSession()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to request new session: %s", err)
|
||||
}
|
||||
defer session.Close()
|
||||
if err := session.Shell(); err != nil {
|
||||
t.Fatalf("Unable to execute command: %s", err)
|
||||
}
|
||||
err = session.Wait()
|
||||
if err == nil {
|
||||
t.Fatalf("expected command to fail but it didn't")
|
||||
}
|
||||
e, ok := err.(*ExitError)
|
||||
if !ok {
|
||||
t.Fatalf("expected *ExitError but got %T", err)
|
||||
}
|
||||
if e.ExitStatus() != 15 {
|
||||
t.Fatalf("expected command to exit with 15 but got %s", e.ExitStatus())
|
||||
}
|
||||
}
|
||||
|
||||
// Test 0 exit status is returned correctly.
|
||||
func TestExitStatusZero(t *testing.T) {
|
||||
conn := dial(exitStatusZeroHandler, t)
|
||||
defer conn.Close()
|
||||
session, err := conn.NewSession()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to request new session: %s", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
if err := session.Shell(); err != nil {
|
||||
t.Fatalf("Unable to execute command: %s", err)
|
||||
}
|
||||
err = session.Wait()
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil but got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test exit signal and status are both returned correctly.
|
||||
func TestExitSignalAndStatus(t *testing.T) {
|
||||
conn := dial(exitSignalAndStatusHandler, t)
|
||||
defer conn.Close()
|
||||
session, err := conn.NewSession()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to request new session: %s", err)
|
||||
}
|
||||
defer session.Close()
|
||||
if err := session.Shell(); err != nil {
|
||||
t.Fatalf("Unable to execute command: %s", err)
|
||||
}
|
||||
err = session.Wait()
|
||||
if err == nil {
|
||||
t.Fatalf("expected command to fail but it didn't")
|
||||
}
|
||||
e, ok := err.(*ExitError)
|
||||
if !ok {
|
||||
t.Fatalf("expected *ExitError but got %T", err)
|
||||
}
|
||||
if e.Signal() != "TERM" || e.ExitStatus() != 15 {
|
||||
t.Fatalf("expected command to exit with signal TERM and status 15 but got signal %s and status %v", e.Signal(), e.ExitStatus())
|
||||
}
|
||||
}
|
||||
|
||||
// Test exit signal and status are both returned correctly.
|
||||
func TestKnownExitSignalOnly(t *testing.T) {
|
||||
conn := dial(exitSignalHandler, t)
|
||||
defer conn.Close()
|
||||
session, err := conn.NewSession()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to request new session: %s", err)
|
||||
}
|
||||
defer session.Close()
|
||||
if err := session.Shell(); err != nil {
|
||||
t.Fatalf("Unable to execute command: %s", err)
|
||||
}
|
||||
err = session.Wait()
|
||||
if err == nil {
|
||||
t.Fatalf("expected command to fail but it didn't")
|
||||
}
|
||||
e, ok := err.(*ExitError)
|
||||
if !ok {
|
||||
t.Fatalf("expected *ExitError but got %T", err)
|
||||
}
|
||||
if e.Signal() != "TERM" || e.ExitStatus() != 143 {
|
||||
t.Fatalf("expected command to exit with signal TERM and status 143 but got signal %s and status %v", e.Signal(), e.ExitStatus())
|
||||
}
|
||||
}
|
||||
|
||||
// Test exit signal and status are both returned correctly.
|
||||
func TestUnknownExitSignal(t *testing.T) {
|
||||
conn := dial(exitSignalUnknownHandler, t)
|
||||
defer conn.Close()
|
||||
session, err := conn.NewSession()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to request new session: %s", err)
|
||||
}
|
||||
defer session.Close()
|
||||
if err := session.Shell(); err != nil {
|
||||
t.Fatalf("Unable to execute command: %s", err)
|
||||
}
|
||||
err = session.Wait()
|
||||
if err == nil {
|
||||
t.Fatalf("expected command to fail but it didn't")
|
||||
}
|
||||
e, ok := err.(*ExitError)
|
||||
if !ok {
|
||||
t.Fatalf("expected *ExitError but got %T", err)
|
||||
}
|
||||
if e.Signal() != "SYS" || e.ExitStatus() != 128 {
|
||||
t.Fatalf("expected command to exit with signal SYS and status 128 but got signal %s and status %v", e.Signal(), e.ExitStatus())
|
||||
}
|
||||
}
|
||||
|
||||
// Test WaitMsg is not returned if the channel closes abruptly.
|
||||
func TestExitWithoutStatusOrSignal(t *testing.T) {
|
||||
conn := dial(exitWithoutSignalOrStatus, t)
|
||||
defer conn.Close()
|
||||
session, err := conn.NewSession()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to request new session: %s", err)
|
||||
}
|
||||
defer session.Close()
|
||||
if err := session.Shell(); err != nil {
|
||||
t.Fatalf("Unable to execute command: %s", err)
|
||||
}
|
||||
err = session.Wait()
|
||||
if err == nil {
|
||||
t.Fatalf("expected command to fail but it didn't")
|
||||
}
|
||||
_, ok := err.(*ExitError)
|
||||
if ok {
|
||||
// you can't actually test for errors.errorString
|
||||
// because it's not exported.
|
||||
t.Fatalf("expected *errorString but got %T", err)
|
||||
}
|
||||
}
|
||||
|
||||
type exitStatusMsg struct {
|
||||
PeersId uint32
|
||||
Request string
|
||||
WantReply bool
|
||||
Status uint32
|
||||
}
|
||||
|
||||
type exitSignalMsg struct {
|
||||
PeersId uint32
|
||||
Request string
|
||||
WantReply bool
|
||||
Signal string
|
||||
CoreDumped bool
|
||||
Errmsg string
|
||||
Lang string
|
||||
}
|
||||
|
||||
func newServerShell(ch *channel, prompt string) *ServerTerminal {
|
||||
term := terminal.NewTerminal(ch, prompt)
|
||||
return &ServerTerminal{
|
||||
Term: term,
|
||||
Channel: ch,
|
||||
}
|
||||
}
|
||||
|
||||
func exitStatusZeroHandler(ch *channel) {
|
||||
defer ch.Close()
|
||||
// this string is returned to stdout
|
||||
shell := newServerShell(ch, "> ")
|
||||
shell.ReadLine()
|
||||
sendStatus(0, ch)
|
||||
}
|
||||
|
||||
func exitStatusNonZeroHandler(ch *channel) {
|
||||
defer ch.Close()
|
||||
shell := newServerShell(ch, "> ")
|
||||
shell.ReadLine()
|
||||
sendStatus(15, ch)
|
||||
}
|
||||
|
||||
func exitSignalAndStatusHandler(ch *channel) {
|
||||
defer ch.Close()
|
||||
shell := newServerShell(ch, "> ")
|
||||
shell.ReadLine()
|
||||
sendStatus(15, ch)
|
||||
sendSignal("TERM", ch)
|
||||
}
|
||||
|
||||
func exitSignalHandler(ch *channel) {
|
||||
defer ch.Close()
|
||||
shell := newServerShell(ch, "> ")
|
||||
shell.ReadLine()
|
||||
sendSignal("TERM", ch)
|
||||
}
|
||||
|
||||
func exitSignalUnknownHandler(ch *channel) {
|
||||
defer ch.Close()
|
||||
shell := newServerShell(ch, "> ")
|
||||
shell.ReadLine()
|
||||
sendSignal("SYS", ch)
|
||||
}
|
||||
|
||||
func exitWithoutSignalOrStatus(ch *channel) {
|
||||
defer ch.Close()
|
||||
shell := newServerShell(ch, "> ")
|
||||
shell.ReadLine()
|
||||
}
|
||||
|
||||
func shellHandler(ch *channel) {
|
||||
defer ch.Close()
|
||||
// this string is returned to stdout
|
||||
shell := newServerShell(ch, "golang")
|
||||
shell.ReadLine()
|
||||
sendStatus(0, ch)
|
||||
}
|
||||
|
||||
func sendStatus(status uint32, ch *channel) {
|
||||
msg := exitStatusMsg{
|
||||
PeersId: ch.theirId,
|
||||
Request: "exit-status",
|
||||
WantReply: false,
|
||||
Status: status,
|
||||
}
|
||||
ch.serverConn.writePacket(marshal(msgChannelRequest, msg))
|
||||
}
|
||||
|
||||
func sendSignal(signal string, ch *channel) {
|
||||
sig := exitSignalMsg{
|
||||
PeersId: ch.theirId,
|
||||
Request: "exit-signal",
|
||||
WantReply: false,
|
||||
Signal: signal,
|
||||
CoreDumped: false,
|
||||
Errmsg: "Process terminated",
|
||||
Lang: "en-GB-oed",
|
||||
}
|
||||
ch.serverConn.writePacket(marshal(msgChannelRequest, sig))
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Dial initiates a connection to the addr from the remote host.
|
||||
// addr is resolved using net.ResolveTCPAddr before connection.
|
||||
// This could allow an observer to observe the DNS name of the
|
||||
// remote host. Consider using ssh.DialTCP to avoid this.
|
||||
func (c *ClientConn) Dial(n, addr string) (net.Conn, error) {
|
||||
raddr, err := net.ResolveTCPAddr(n, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.DialTCP(n, nil, raddr)
|
||||
}
|
||||
|
||||
// DialTCP connects to the remote address raddr on the network net,
|
||||
// which must be "tcp", "tcp4", or "tcp6". If laddr is not nil, it is used
|
||||
// as the local address for the connection.
|
||||
func (c *ClientConn) DialTCP(n string, laddr, raddr *net.TCPAddr) (net.Conn, error) {
|
||||
if laddr == nil {
|
||||
laddr = &net.TCPAddr{
|
||||
IP: net.IPv4zero,
|
||||
Port: 0,
|
||||
}
|
||||
}
|
||||
ch, err := c.dial(laddr.IP.String(), laddr.Port, raddr.IP.String(), raddr.Port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tcpchanconn{
|
||||
tcpchan: ch,
|
||||
laddr: laddr,
|
||||
raddr: raddr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RFC 4254 7.2
|
||||
type channelOpenDirectMsg struct {
|
||||
ChanType string
|
||||
PeersId uint32
|
||||
PeersWindow uint32
|
||||
MaxPacketSize uint32
|
||||
raddr string
|
||||
rport uint32
|
||||
laddr string
|
||||
lport uint32
|
||||
}
|
||||
|
||||
// dial opens a direct-tcpip connection to the remote server. laddr and raddr are passed as
|
||||
// strings and are expected to be resolveable at the remote end.
|
||||
func (c *ClientConn) dial(laddr string, lport int, raddr string, rport int) (*tcpchan, error) {
|
||||
ch := c.newChan(c.transport)
|
||||
if err := c.writePacket(marshal(msgChannelOpen, channelOpenDirectMsg{
|
||||
ChanType: "direct-tcpip",
|
||||
PeersId: ch.id,
|
||||
PeersWindow: 1 << 14,
|
||||
MaxPacketSize: 1 << 15, // RFC 4253 6.1
|
||||
raddr: raddr,
|
||||
rport: uint32(rport),
|
||||
laddr: laddr,
|
||||
lport: uint32(lport),
|
||||
})); err != nil {
|
||||
c.chanlist.remove(ch.id)
|
||||
return nil, err
|
||||
}
|
||||
if err := ch.waitForChannelOpenResponse(); err != nil {
|
||||
c.chanlist.remove(ch.id)
|
||||
return nil, fmt.Errorf("ssh: unable to open direct tcpip connection: %v", err)
|
||||
}
|
||||
return &tcpchan{
|
||||
clientChan: ch,
|
||||
Reader: ch.stdout,
|
||||
Writer: ch.stdin,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type tcpchan struct {
|
||||
*clientChan // the backing channel
|
||||
io.Reader
|
||||
io.Writer
|
||||
}
|
||||
|
||||
// tcpchanconn fulfills the net.Conn interface without
|
||||
// the tcpchan having to hold laddr or raddr directly.
|
||||
type tcpchanconn struct {
|
||||
*tcpchan
|
||||
laddr, raddr net.Addr
|
||||
}
|
||||
|
||||
// LocalAddr returns the local network address.
|
||||
func (t *tcpchanconn) LocalAddr() net.Addr {
|
||||
return t.laddr
|
||||
}
|
||||
|
||||
// RemoteAddr returns the remote network address.
|
||||
func (t *tcpchanconn) 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 {
|
||||
if err := t.SetReadDeadline(deadline); err != nil {
|
||||
return err
|
||||
}
|
||||
return t.SetWriteDeadline(deadline)
|
||||
}
|
||||
|
||||
// SetReadDeadline sets the read deadline.
|
||||
// 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 {
|
||||
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 {
|
||||
return errors.New("ssh: tcpchan: deadline not supported")
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
// direct-tcpip functional tests
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTCPIPHTTP(t *testing.T) {
|
||||
if *sshuser == "" {
|
||||
t.Log("ssh.user not defined, skipping test")
|
||||
return
|
||||
}
|
||||
// google.com will generate at least one redirect, possibly three
|
||||
// depending on your location.
|
||||
doTest(t, "http://google.com")
|
||||
}
|
||||
|
||||
func TestTCPIPHTTPS(t *testing.T) {
|
||||
if *sshuser == "" {
|
||||
t.Log("ssh.user not defined, skipping test")
|
||||
return
|
||||
}
|
||||
doTest(t, "https://encrypted.google.com/")
|
||||
}
|
||||
|
||||
func doTest(t *testing.T, url string) {
|
||||
config := &ClientConfig{
|
||||
User: *sshuser,
|
||||
Auth: []ClientAuth{
|
||||
ClientAuthPassword(password(*sshpass)),
|
||||
},
|
||||
}
|
||||
conn, err := Dial("tcp", "localhost:22", config)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to connect: %s", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
tr := &http.Transport{
|
||||
Dial: func(n, addr string) (net.Conn, error) {
|
||||
return conn.Dial(n, addr)
|
||||
},
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: tr,
|
||||
}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to proxy: %s", err)
|
||||
}
|
||||
// got a body without error
|
||||
t.Log(resp)
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"hash"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
packetSizeMultiple = 16 // TODO(huin) this should be determined by the cipher.
|
||||
minPacketSize = 16
|
||||
maxPacketSize = 36000
|
||||
minPaddingSize = 4 // TODO(huin) should this be configurable?
|
||||
)
|
||||
|
||||
// filteredConn reduces the set of methods exposed when embeddeding
|
||||
// a net.Conn inside ssh.transport.
|
||||
// TODO(dfc) suggestions for a better name will be warmly received.
|
||||
type filteredConn interface {
|
||||
// Close closes the connection.
|
||||
Close() error
|
||||
|
||||
// LocalAddr returns the local network address.
|
||||
LocalAddr() net.Addr
|
||||
|
||||
// RemoteAddr returns the remote network address.
|
||||
RemoteAddr() net.Addr
|
||||
}
|
||||
|
||||
// Types implementing packetWriter provide the ability to send packets to
|
||||
// an SSH peer.
|
||||
type packetWriter interface {
|
||||
// Encrypt and send a packet of data to the remote peer.
|
||||
writePacket(packet []byte) error
|
||||
}
|
||||
|
||||
// transport represents the SSH connection to the remote peer.
|
||||
type transport struct {
|
||||
reader
|
||||
writer
|
||||
|
||||
filteredConn
|
||||
}
|
||||
|
||||
// reader represents the incoming connection state.
|
||||
type reader struct {
|
||||
io.Reader
|
||||
common
|
||||
}
|
||||
|
||||
// writer represnts the outgoing connection state.
|
||||
type writer struct {
|
||||
*sync.Mutex // protects writer.Writer from concurrent writes
|
||||
*bufio.Writer
|
||||
rand io.Reader
|
||||
common
|
||||
}
|
||||
|
||||
// common represents the cipher state needed to process messages in a single
|
||||
// direction.
|
||||
type common struct {
|
||||
seqNum uint32
|
||||
mac hash.Hash
|
||||
cipher cipher.Stream
|
||||
|
||||
cipherAlgo string
|
||||
macAlgo string
|
||||
compressionAlgo string
|
||||
}
|
||||
|
||||
// Read and decrypt a single packet from the remote peer.
|
||||
func (r *reader) readOnePacket() ([]byte, error) {
|
||||
var lengthBytes = make([]byte, 5)
|
||||
var macSize uint32
|
||||
if _, err := io.ReadFull(r, lengthBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.cipher.XORKeyStream(lengthBytes, lengthBytes)
|
||||
|
||||
if r.mac != nil {
|
||||
r.mac.Reset()
|
||||
seqNumBytes := []byte{
|
||||
byte(r.seqNum >> 24),
|
||||
byte(r.seqNum >> 16),
|
||||
byte(r.seqNum >> 8),
|
||||
byte(r.seqNum),
|
||||
}
|
||||
r.mac.Write(seqNumBytes)
|
||||
r.mac.Write(lengthBytes)
|
||||
macSize = uint32(r.mac.Size())
|
||||
}
|
||||
|
||||
length := uint32(lengthBytes[0])<<24 | uint32(lengthBytes[1])<<16 | uint32(lengthBytes[2])<<8 | uint32(lengthBytes[3])
|
||||
paddingLength := uint32(lengthBytes[4])
|
||||
|
||||
if length <= paddingLength+1 {
|
||||
return nil, errors.New("invalid packet length")
|
||||
}
|
||||
if length > maxPacketSize {
|
||||
return nil, errors.New("packet too large")
|
||||
}
|
||||
|
||||
packet := make([]byte, length-1+macSize)
|
||||
if _, err := io.ReadFull(r, packet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mac := packet[length-1:]
|
||||
r.cipher.XORKeyStream(packet, packet[:length-1])
|
||||
|
||||
if r.mac != nil {
|
||||
r.mac.Write(packet[:length-1])
|
||||
if subtle.ConstantTimeCompare(r.mac.Sum(nil), mac) != 1 {
|
||||
return nil, errors.New("ssh: MAC failure")
|
||||
}
|
||||
}
|
||||
|
||||
r.seqNum++
|
||||
return packet[:length-paddingLength-1], nil
|
||||
}
|
||||
|
||||
// Read and decrypt next packet discarding debug and noop messages.
|
||||
func (t *transport) readPacket() ([]byte, error) {
|
||||
for {
|
||||
packet, err := t.readOnePacket()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if packet[0] != msgIgnore && packet[0] != msgDebug {
|
||||
return packet, nil
|
||||
}
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// Encrypt and send a packet of data to the remote peer.
|
||||
func (w *writer) writePacket(packet []byte) error {
|
||||
w.Mutex.Lock()
|
||||
defer w.Mutex.Unlock()
|
||||
|
||||
paddingLength := packetSizeMultiple - (5+len(packet))%packetSizeMultiple
|
||||
if paddingLength < 4 {
|
||||
paddingLength += packetSizeMultiple
|
||||
}
|
||||
|
||||
length := len(packet) + 1 + paddingLength
|
||||
lengthBytes := []byte{
|
||||
byte(length >> 24),
|
||||
byte(length >> 16),
|
||||
byte(length >> 8),
|
||||
byte(length),
|
||||
byte(paddingLength),
|
||||
}
|
||||
padding := make([]byte, paddingLength)
|
||||
_, err := io.ReadFull(w.rand, padding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if w.mac != nil {
|
||||
w.mac.Reset()
|
||||
seqNumBytes := []byte{
|
||||
byte(w.seqNum >> 24),
|
||||
byte(w.seqNum >> 16),
|
||||
byte(w.seqNum >> 8),
|
||||
byte(w.seqNum),
|
||||
}
|
||||
w.mac.Write(seqNumBytes)
|
||||
w.mac.Write(lengthBytes)
|
||||
w.mac.Write(packet)
|
||||
w.mac.Write(padding)
|
||||
}
|
||||
|
||||
// TODO(dfc) lengthBytes, packet and padding should be
|
||||
// subslices of a single buffer
|
||||
w.cipher.XORKeyStream(lengthBytes, lengthBytes)
|
||||
w.cipher.XORKeyStream(packet, packet)
|
||||
w.cipher.XORKeyStream(padding, padding)
|
||||
|
||||
if _, err := w.Write(lengthBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write(packet); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write(padding); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if w.mac != nil {
|
||||
if _, err := w.Write(w.mac.Sum(nil)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
w.seqNum++
|
||||
return err
|
||||
}
|
||||
|
||||
// Send a message to the remote peer
|
||||
func (t *transport) sendMessage(typ uint8, msg interface{}) error {
|
||||
packet := marshal(typ, msg)
|
||||
return t.writePacket(packet)
|
||||
}
|
||||
|
||||
func newTransport(conn net.Conn, rand io.Reader) *transport {
|
||||
return &transport{
|
||||
reader: reader{
|
||||
Reader: bufio.NewReader(conn),
|
||||
common: common{
|
||||
cipher: noneCipher{},
|
||||
},
|
||||
},
|
||||
writer: writer{
|
||||
Writer: bufio.NewWriter(conn),
|
||||
rand: rand,
|
||||
Mutex: new(sync.Mutex),
|
||||
common: common{
|
||||
cipher: noneCipher{},
|
||||
},
|
||||
},
|
||||
filteredConn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
type direction struct {
|
||||
ivTag []byte
|
||||
keyTag []byte
|
||||
macKeyTag []byte
|
||||
}
|
||||
|
||||
// TODO(dfc) can this be made a constant ?
|
||||
var (
|
||||
serverKeys = direction{[]byte{'B'}, []byte{'D'}, []byte{'F'}}
|
||||
clientKeys = direction{[]byte{'A'}, []byte{'C'}, []byte{'E'}}
|
||||
)
|
||||
|
||||
// setupKeys sets the cipher and MAC keys from kex.K, kex.H and sessionId, as
|
||||
// described in RFC 4253, section 6.4. direction should either be serverKeys
|
||||
// (to setup server->client keys) or clientKeys (for client->server keys).
|
||||
func (c *common) setupKeys(d direction, K, H, sessionId []byte, hashFunc crypto.Hash) error {
|
||||
cipherMode := cipherModes[c.cipherAlgo]
|
||||
|
||||
macKeySize := 20
|
||||
|
||||
iv := make([]byte, cipherMode.ivSize)
|
||||
key := make([]byte, cipherMode.keySize)
|
||||
macKey := make([]byte, macKeySize)
|
||||
|
||||
h := hashFunc.New()
|
||||
generateKeyMaterial(iv, d.ivTag, K, H, sessionId, h)
|
||||
generateKeyMaterial(key, d.keyTag, K, H, sessionId, h)
|
||||
generateKeyMaterial(macKey, d.macKeyTag, K, H, sessionId, h)
|
||||
|
||||
c.mac = truncatingMAC{12, hmac.New(sha1.New, macKey)}
|
||||
|
||||
cipher, err := cipherMode.createCipher(key, iv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.cipher = cipher
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateKeyMaterial fills out with key material generated from tag, K, H
|
||||
// and sessionId, as specified in RFC 4253, section 7.2.
|
||||
func generateKeyMaterial(out, tag []byte, K, H, sessionId []byte, h hash.Hash) {
|
||||
var digestsSoFar []byte
|
||||
|
||||
for len(out) > 0 {
|
||||
h.Reset()
|
||||
h.Write(K)
|
||||
h.Write(H)
|
||||
|
||||
if len(digestsSoFar) == 0 {
|
||||
h.Write(tag)
|
||||
h.Write(sessionId)
|
||||
} else {
|
||||
h.Write(digestsSoFar)
|
||||
}
|
||||
|
||||
digest := h.Sum(nil)
|
||||
n := copy(out, digest)
|
||||
out = out[n:]
|
||||
if len(out) > 0 {
|
||||
digestsSoFar = append(digestsSoFar, digest...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// truncatingMAC wraps around a hash.Hash and truncates the output digest to
|
||||
// a given size.
|
||||
type truncatingMAC struct {
|
||||
length int
|
||||
hmac hash.Hash
|
||||
}
|
||||
|
||||
func (t truncatingMAC) Write(data []byte) (int, error) {
|
||||
return t.hmac.Write(data)
|
||||
}
|
||||
|
||||
func (t truncatingMAC) Sum(in []byte) []byte {
|
||||
out := t.hmac.Sum(in)
|
||||
return out[:len(in)+t.length]
|
||||
}
|
||||
|
||||
func (t truncatingMAC) Reset() {
|
||||
t.hmac.Reset()
|
||||
}
|
||||
|
||||
func (t truncatingMAC) Size() int {
|
||||
return t.length
|
||||
}
|
||||
|
||||
func (t truncatingMAC) BlockSize() int { return t.hmac.BlockSize() }
|
||||
|
||||
// maxVersionStringBytes is the maximum number of bytes that we'll accept as a
|
||||
// version string. In the event that the client is talking a different protocol
|
||||
// we need to set a limit otherwise we will keep using more and more memory
|
||||
// while searching for the end of the version handshake.
|
||||
const maxVersionStringBytes = 1024
|
||||
|
||||
// Read version string as specified by RFC 4253, section 4.2.
|
||||
func readVersion(r io.Reader) ([]byte, error) {
|
||||
versionString := make([]byte, 0, 64)
|
||||
var ok bool
|
||||
var buf [1]byte
|
||||
forEachByte:
|
||||
for len(versionString) < maxVersionStringBytes {
|
||||
_, err := io.ReadFull(r, buf[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The RFC says that the version should be terminated with \r\n
|
||||
// but several SSH servers actually only send a \n.
|
||||
if buf[0] == '\n' {
|
||||
ok = true
|
||||
break forEachByte
|
||||
}
|
||||
versionString = append(versionString, buf[0])
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return nil, errors.New("ssh: failed to read version string")
|
||||
}
|
||||
|
||||
// There might be a '\r' on the end which we should remove.
|
||||
if len(versionString) > 0 && versionString[len(versionString)-1] == '\r' {
|
||||
versionString = versionString[:len(versionString)-1]
|
||||
}
|
||||
return versionString, nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2011 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 ssh
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadVersion(t *testing.T) {
|
||||
buf := serverVersion
|
||||
result, err := readVersion(bufio.NewReader(bytes.NewBuffer(buf)))
|
||||
if err != nil {
|
||||
t.Errorf("readVersion didn't read version correctly: %s", err)
|
||||
}
|
||||
if !bytes.Equal(buf[:len(buf)-2], result) {
|
||||
t.Error("version read did not match expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadVersionWithJustLF(t *testing.T) {
|
||||
var buf []byte
|
||||
buf = append(buf, serverVersion...)
|
||||
buf = buf[:len(buf)-1]
|
||||
buf[len(buf)-1] = '\n'
|
||||
result, err := readVersion(bufio.NewReader(bytes.NewBuffer(buf)))
|
||||
if err != nil {
|
||||
t.Error("readVersion failed to handle just a \n")
|
||||
}
|
||||
if !bytes.Equal(buf[:len(buf)-1], result) {
|
||||
t.Errorf("version read did not match expected: got %x, want %x", result, buf[:len(buf)-1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadVersionTooLong(t *testing.T) {
|
||||
buf := make([]byte, maxVersionStringBytes+1)
|
||||
if _, err := readVersion(bufio.NewReader(bytes.NewBuffer(buf))); err == nil {
|
||||
t.Errorf("readVersion consumed %d bytes without error", len(buf))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadVersionWithoutCRLF(t *testing.T) {
|
||||
buf := serverVersion
|
||||
buf = buf[:len(buf)-1]
|
||||
if _, err := readVersion(bufio.NewReader(bytes.NewBuffer(buf))); err == nil {
|
||||
t.Error("readVersion did not notice \\n was missing")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user