mirror of
https://github.com/golang/go
synced 2024-11-21 21:04:41 -07:00
exp/ssh: new package.
The typical UNIX method for controlling long running process is to send the process signals. Since this doesn't get you very far, various ad-hoc, remote-control protocols have been used over time by programs like Apache and BIND. Implementing an SSH server means that Go code will have a standard, secure way to do this in the future. R=bradfitz, borman, dave, gustavo, dsymonds, r, adg, rsc, rogpeppe, lvd, kevlar, raul.san CC=golang-dev https://golang.org/cl/4962064
This commit is contained in:
parent
b71a805cd5
commit
605e57d8fe
16
src/pkg/exp/ssh/Makefile
Normal file
16
src/pkg/exp/ssh/Makefile
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
include ../../../Make.inc
|
||||||
|
|
||||||
|
TARG=exp/ssh
|
||||||
|
GOFILES=\
|
||||||
|
common.go\
|
||||||
|
messages.go\
|
||||||
|
server.go\
|
||||||
|
transport.go\
|
||||||
|
channel.go\
|
||||||
|
server_shell.go\
|
||||||
|
|
||||||
|
include ../../../Make.pkg
|
317
src/pkg/exp/ssh/channel.go
Normal file
317
src/pkg/exp/ssh/channel.go
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
// 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 (
|
||||||
|
"os"
|
||||||
|
"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() os.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) os.Error
|
||||||
|
|
||||||
|
// Read may return a ChannelRequest as an os.Error.
|
||||||
|
Read(data []byte) (int, os.Error)
|
||||||
|
Write(data []byte) (int, os.Error)
|
||||||
|
Close() os.Error
|
||||||
|
|
||||||
|
// AckRequest either sends an ack or nack to the channel request.
|
||||||
|
AckRequest(ok bool) os.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) String() 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 *ServerConnection
|
||||||
|
myId, theirId uint32
|
||||||
|
myWindow, theirWindow uint32
|
||||||
|
maxPacketSize uint32
|
||||||
|
err os.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() os.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.out.writePacket(marshal(msgChannelOpenConfirm, confirm))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *channel) Reject(reason RejectionReason, message string) os.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.out.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 os.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.out.writePacket(packet); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if c.theySentEOF || c.theyClosed || c.dead {
|
||||||
|
return 0, os.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 os.Error) {
|
||||||
|
for len(data) > 0 {
|
||||||
|
c.lock.Lock()
|
||||||
|
if c.dead || c.weClosed {
|
||||||
|
return 0, os.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.out.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() os.Error {
|
||||||
|
c.serverConn.lock.Lock()
|
||||||
|
defer c.serverConn.lock.Unlock()
|
||||||
|
|
||||||
|
if c.serverConn.err != nil {
|
||||||
|
return c.serverConn.err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.weClosed {
|
||||||
|
return os.NewError("ssh: channel already closed")
|
||||||
|
}
|
||||||
|
c.weClosed = true
|
||||||
|
|
||||||
|
closeMsg := channelCloseMsg{
|
||||||
|
PeersId: c.theirId,
|
||||||
|
}
|
||||||
|
return c.serverConn.out.writePacket(marshal(msgChannelClose, closeMsg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *channel) AckRequest(ok bool) os.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.out.writePacket(marshal(msgChannelSuccess, ack))
|
||||||
|
} else {
|
||||||
|
ack := channelRequestFailureMsg{
|
||||||
|
PeersId: c.theirId,
|
||||||
|
}
|
||||||
|
return c.serverConn.out.writePacket(marshal(msgChannelFailure, ack))
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *channel) ChannelType() string {
|
||||||
|
return c.chanType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *channel) ExtraData() []byte {
|
||||||
|
return c.extraData
|
||||||
|
}
|
96
src/pkg/exp/ssh/common.go
Normal file
96
src/pkg/exp/ssh/common.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// 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 (
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These are string constants in the SSH protocol.
|
||||||
|
const (
|
||||||
|
kexAlgoDH14SHA1 = "diffie-hellman-group14-sha1"
|
||||||
|
hostAlgoRSA = "ssh-rsa"
|
||||||
|
cipherAES128CTR = "aes128-ctr"
|
||||||
|
macSHA196 = "hmac-sha1-96"
|
||||||
|
compressionNone = "none"
|
||||||
|
serviceUserAuth = "ssh-userauth"
|
||||||
|
serviceSSH = "ssh-connection"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnexpectedMessageError results when the SSH message that we received didn't
|
||||||
|
// match what we wanted.
|
||||||
|
type UnexpectedMessageError struct {
|
||||||
|
expected, got uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UnexpectedMessageError) String() 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) String() string {
|
||||||
|
return "ssh: parse error in message type " + strconv.Itoa(int(p.msgType))
|
||||||
|
}
|
||||||
|
|
||||||
|
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(clientToServer, serverToClient *halfConnection, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
clientToServer.cipherAlgo, ok = findCommonAlgorithm(clientKexInit.CiphersClientServer, serverKexInit.CiphersClientServer)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverToClient.cipherAlgo, ok = findCommonAlgorithm(clientKexInit.CiphersServerClient, serverKexInit.CiphersServerClient)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientToServer.macAlgo, ok = findCommonAlgorithm(clientKexInit.MACsClientServer, serverKexInit.MACsClientServer)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverToClient.macAlgo, ok = findCommonAlgorithm(clientKexInit.MACsServerClient, serverKexInit.MACsServerClient)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientToServer.compressionAlgo, ok = findCommonAlgorithm(clientKexInit.CompressionClientServer, serverKexInit.CompressionClientServer)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverToClient.compressionAlgo, ok = findCommonAlgorithm(clientKexInit.CompressionServerClient, serverKexInit.CompressionServerClient)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
79
src/pkg/exp/ssh/doc.go
Normal file
79
src/pkg/exp/ssh/doc.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// 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 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 Server, which manages a number of
|
||||||
|
ServerConnections and handles authentication.
|
||||||
|
|
||||||
|
var s Server
|
||||||
|
s.PubKeyCallback = pubKeyAuth
|
||||||
|
s.PasswordCallback = passwordAuth
|
||||||
|
|
||||||
|
pemBytes, err := ioutil.ReadFile("id_rsa")
|
||||||
|
if err != nil {
|
||||||
|
panic("Failed to load private key")
|
||||||
|
}
|
||||||
|
err = s.SetRSAPrivateKey(pemBytes)
|
||||||
|
if err != nil {
|
||||||
|
panic("Failed to parse private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
Once a Server has been set up, connections can be attached.
|
||||||
|
|
||||||
|
var sConn ServerConnection
|
||||||
|
sConn.Server = &s
|
||||||
|
err = sConn.Handshake(conn)
|
||||||
|
if 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" {
|
||||||
|
c.Reject(RejectUnknownChannelType, "unknown channel type")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channel.Accept()
|
||||||
|
|
||||||
|
shell := NewServerShell(channel, "> ")
|
||||||
|
go func() {
|
||||||
|
defer channel.Close()
|
||||||
|
for {
|
||||||
|
line, err := shell.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
println(line)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}()
|
||||||
|
*/
|
||||||
|
package ssh
|
557
src/pkg/exp/ssh/messages.go
Normal file
557
src/pkg/exp/ssh/messages.go
Normal file
@ -0,0 +1,557 @@
|
|||||||
|
// 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 (
|
||||||
|
"big"
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"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
|
||||||
|
// tag of "rest" receives the remainder of a packet when unmarshaling.
|
||||||
|
|
||||||
|
// 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 "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 "rest"
|
||||||
|
}
|
||||||
|
|
||||||
|
// See RFC 4254, section 5.1.
|
||||||
|
type channelOpenConfirmMsg struct {
|
||||||
|
PeersId uint32
|
||||||
|
MyId uint32
|
||||||
|
MyWindow uint32
|
||||||
|
MaxPacketSize uint32
|
||||||
|
TypeSpecificData []byte "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 "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) os.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 == "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, []byte(s)...)
|
||||||
|
case reflect.Slice:
|
||||||
|
switch t.Elem().Kind() {
|
||||||
|
case reflect.Uint8:
|
||||||
|
length := field.Len()
|
||||||
|
if structType.Field(i).Tag != "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, []byte(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{','}
|
||||||
|
|
||||||
|
func parseNameList(in []byte) (out []string, rest []byte, ok bool) {
|
||||||
|
contents, rest, ok := parseString(in)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(contents) == 0 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxPacketSize = 36000
|
||||||
|
|
||||||
|
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 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))
|
125
src/pkg/exp/ssh/messages_test.go
Normal file
125
src/pkg/exp/ssh/messages_test.go
Normal file
@ -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 (
|
||||||
|
"big"
|
||||||
|
"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)
|
||||||
|
}
|
711
src/pkg/exp/ssh/server.go
Normal file
711
src/pkg/exp/ssh/server.go
Normal file
@ -0,0 +1,711 @@
|
|||||||
|
// 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 (
|
||||||
|
"big"
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
_ "crypto/sha1"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var supportedKexAlgos = []string{kexAlgoDH14SHA1}
|
||||||
|
var supportedHostKeyAlgos = []string{hostAlgoRSA}
|
||||||
|
var supportedCiphers = []string{cipherAES128CTR}
|
||||||
|
var supportedMACs = []string{macSHA196}
|
||||||
|
var supportedCompressions = []string{compressionNone}
|
||||||
|
|
||||||
|
// Server represents an SSH server. A Server may have several ServerConnections.
|
||||||
|
type Server struct {
|
||||||
|
rsa *rsa.PrivateKey
|
||||||
|
rsaSerialized []byte
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// PubKeyCallback, 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.
|
||||||
|
PubKeyCallback func(user, algo string, pubkey []byte) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 *Server) SetRSAPrivateKey(pemBytes []byte) os.Error {
|
||||||
|
block, _ := pem.Decode(pemBytes)
|
||||||
|
if block == nil {
|
||||||
|
return os.NewError("ssh: no key found")
|
||||||
|
}
|
||||||
|
var err os.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 ServerConnection.
|
||||||
|
type cachedPubKey struct {
|
||||||
|
user, algo string
|
||||||
|
pubKey []byte
|
||||||
|
result bool
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxCachedPubKeys = 16
|
||||||
|
|
||||||
|
// ServerConnection represents an incomming connection to a Server.
|
||||||
|
type ServerConnection struct {
|
||||||
|
Server *Server
|
||||||
|
|
||||||
|
in, out *halfConnection
|
||||||
|
|
||||||
|
channels map[uint32]*channel
|
||||||
|
nextChanId uint32
|
||||||
|
|
||||||
|
// lock protects err and also allows Channels to serialise their writes
|
||||||
|
// to out.
|
||||||
|
lock sync.RWMutex
|
||||||
|
err os.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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type handshakeMagics struct {
|
||||||
|
clientVersion, serverVersion []byte
|
||||||
|
clientKexInit, serverKexInit []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 *ServerConnection) kexDH(group *dhGroup, hashFunc crypto.Hash, magics *handshakeMagics, hostKeyAlgo string) (H, K []byte, err os.Error) {
|
||||||
|
packet, err := s.in.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, os.NewError("client DH parameter out of bounds")
|
||||||
|
}
|
||||||
|
|
||||||
|
y, err := rand.Int(rand.Reader, 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.Server.rsaSerialized
|
||||||
|
default:
|
||||||
|
return nil, nil, os.NewError("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()
|
||||||
|
|
||||||
|
h.Reset()
|
||||||
|
h.Write(H)
|
||||||
|
hh := h.Sum()
|
||||||
|
|
||||||
|
var sig []byte
|
||||||
|
switch hostKeyAlgo {
|
||||||
|
case hostAlgoRSA:
|
||||||
|
sig, err = rsa.SignPKCS1v15(rand.Reader, s.Server.rsa, hashFunc, hh)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, nil, os.NewError("internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
serializedSig := serializeRSASignature(sig)
|
||||||
|
|
||||||
|
kexDHReply := kexDHReplyMsg{
|
||||||
|
HostKey: serializedHostKey,
|
||||||
|
Y: Y,
|
||||||
|
Signature: serializedSig,
|
||||||
|
}
|
||||||
|
packet = marshal(msgKexDHReply, kexDHReply)
|
||||||
|
|
||||||
|
err = s.out.writePacket(packet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func serializeRSASignature(sig []byte) []byte {
|
||||||
|
length := stringLength([]byte(hostAlgoRSA))
|
||||||
|
length += stringLength(sig)
|
||||||
|
|
||||||
|
ret := make([]byte, length)
|
||||||
|
r := marshalString(ret, []byte(hostAlgoRSA))
|
||||||
|
r = marshalString(r, sig)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverVersion is the fixed identification string that Server will use.
|
||||||
|
var serverVersion = []byte("SSH-2.0-Go\r\n")
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handshake performs an SSH transport and client authentication on the given ServerConnection.
|
||||||
|
func (s *ServerConnection) Handshake(conn net.Conn) os.Error {
|
||||||
|
var magics handshakeMagics
|
||||||
|
inBuf := bufio.NewReader(conn)
|
||||||
|
|
||||||
|
_, err := conn.Write(serverVersion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
magics.serverVersion = serverVersion[:len(serverVersion)-2]
|
||||||
|
serverKexInit := kexInitMsg{
|
||||||
|
KexAlgos: supportedKexAlgos,
|
||||||
|
ServerHostKeyAlgos: supportedHostKeyAlgos,
|
||||||
|
CiphersClientServer: supportedCiphers,
|
||||||
|
CiphersServerClient: supportedCiphers,
|
||||||
|
MACsClientServer: supportedMACs,
|
||||||
|
MACsServerClient: supportedMACs,
|
||||||
|
CompressionClientServer: supportedCompressions,
|
||||||
|
CompressionServerClient: supportedCompressions,
|
||||||
|
}
|
||||||
|
kexInitPacket := marshal(msgKexInit, serverKexInit)
|
||||||
|
magics.serverKexInit = kexInitPacket
|
||||||
|
|
||||||
|
var out halfConnection
|
||||||
|
out.out = conn
|
||||||
|
out.rand = rand.Reader
|
||||||
|
s.out = &out
|
||||||
|
err = out.writePacket(kexInitPacket)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
version, ok := readVersion(inBuf)
|
||||||
|
if !ok {
|
||||||
|
return os.NewError("failed to read version string from client")
|
||||||
|
}
|
||||||
|
magics.clientVersion = version
|
||||||
|
|
||||||
|
var in halfConnection
|
||||||
|
in.in = inBuf
|
||||||
|
s.in = &in
|
||||||
|
packet, err := in.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(&in, &out, &clientKexInit, &serverKexInit)
|
||||||
|
if !ok {
|
||||||
|
return os.NewError("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.
|
||||||
|
_, err := in.readPacket()
|
||||||
|
if 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 = os.NewError("ssh: internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
packet = []byte{msgNewKeys}
|
||||||
|
if err = out.writePacket(packet); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = out.setupKeys(serverKeys, K, H, H, hashFunc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if packet, err = in.readPacket(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if packet[0] != msgNewKeys {
|
||||||
|
return UnexpectedMessageError{msgNewKeys, packet[0]}
|
||||||
|
}
|
||||||
|
|
||||||
|
in.setupKeys(clientKeys, K, H, H, hashFunc)
|
||||||
|
|
||||||
|
packet, err = in.readPacket()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceRequest serviceRequestMsg
|
||||||
|
if err = unmarshal(&serviceRequest, packet, msgServiceRequest); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if serviceRequest.Service != serviceUserAuth {
|
||||||
|
return os.NewError("ssh: requested service '" + serviceRequest.Service + "' before authenticating")
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceAccept := serviceAcceptMsg{
|
||||||
|
Service: serviceUserAuth,
|
||||||
|
}
|
||||||
|
packet = marshal(msgServiceAccept, serviceAccept)
|
||||||
|
if err = out.writePacket(packet); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.authenticate(H); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.channels = make(map[uint32]*channel)
|
||||||
|
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 *ServerConnection) testPubKey(user, algo string, pubKey []byte) bool {
|
||||||
|
if s.Server.PubKeyCallback == 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.Server.PubKeyCallback(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 *ServerConnection) authenticate(H []byte) os.Error {
|
||||||
|
var userAuthReq userAuthRequestMsg
|
||||||
|
var err os.Error
|
||||||
|
var packet []byte
|
||||||
|
|
||||||
|
userAuthLoop:
|
||||||
|
for {
|
||||||
|
if packet, err = s.in.readPacket(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = unmarshal(&userAuthReq, packet, msgUserAuthRequest); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if userAuthReq.Service != serviceSSH {
|
||||||
|
return os.NewError("ssh: client attempted to negotiate for unknown service: " + userAuthReq.Service)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch userAuthReq.Method {
|
||||||
|
case "none":
|
||||||
|
if s.Server.NoClientAuth {
|
||||||
|
break userAuthLoop
|
||||||
|
}
|
||||||
|
case "password":
|
||||||
|
if s.Server.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.Server.PasswordCallback(userAuthReq.User, string(password)) {
|
||||||
|
break userAuthLoop
|
||||||
|
}
|
||||||
|
case "publickey":
|
||||||
|
if s.Server.PubKeyCallback == 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.out.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()
|
||||||
|
rsaKey, ok := parseRSA(pubKey)
|
||||||
|
if !ok {
|
||||||
|
return ParseError{msgUserAuthRequest}
|
||||||
|
}
|
||||||
|
if rsa.VerifyPKCS1v15(rsaKey, hashFunc, digest, rsaSig) != nil {
|
||||||
|
return ParseError{msgUserAuthRequest}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return os.NewError("ssh: isAcceptableAlgo incorrect")
|
||||||
|
}
|
||||||
|
if s.testPubKey(userAuthReq.User, algo, pubKey) {
|
||||||
|
break userAuthLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var failureMsg userAuthFailureMsg
|
||||||
|
if s.Server.PasswordCallback != nil {
|
||||||
|
failureMsg.Methods = append(failureMsg.Methods, "password")
|
||||||
|
}
|
||||||
|
if s.Server.PubKeyCallback != nil {
|
||||||
|
failureMsg.Methods = append(failureMsg.Methods, "publickey")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(failureMsg.Methods) == 0 {
|
||||||
|
return os.NewError("ssh: no authentication methods configured but NoClientAuth is also false")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.out.writePacket(marshal(msgUserAuthFailure, failureMsg)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packet = []byte{msgUserAuthSuccess}
|
||||||
|
if err = s.out.writePacket(packet); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultWindowSize = 32768
|
||||||
|
|
||||||
|
// Accept reads and processes messages on a ServerConnection. It must be called
|
||||||
|
// in order to demultiplex messages to any resulting Channels.
|
||||||
|
func (s *ServerConnection) Accept() (Channel, os.Error) {
|
||||||
|
if s.err != nil {
|
||||||
|
return nil, s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
packet, err := s.in.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 msgChannelOpen:
|
||||||
|
var chanOpen channelOpenMsg
|
||||||
|
if err := unmarshal(&chanOpen, packet, msgChannelOpen); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c := new(channel)
|
||||||
|
c.chanType = chanOpen.ChanType
|
||||||
|
c.theirId = chanOpen.PeersId
|
||||||
|
c.theirWindow = chanOpen.PeersWindow
|
||||||
|
c.maxPacketSize = chanOpen.MaxPacketSize
|
||||||
|
c.extraData = chanOpen.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 msgChannelRequest:
|
||||||
|
var chanRequest channelRequestMsg
|
||||||
|
if err := unmarshal(&chanRequest, packet, msgChannelRequest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lock.Lock()
|
||||||
|
c, ok := s.channels[chanRequest.PeersId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.handlePacket(&chanRequest)
|
||||||
|
s.lock.Unlock()
|
||||||
|
|
||||||
|
case msgChannelData:
|
||||||
|
if len(packet) < 5 {
|
||||||
|
return nil, ParseError{msgChannelData}
|
||||||
|
}
|
||||||
|
chanId := uint32(packet[1])<<24 | uint32(packet[2])<<16 | uint32(packet[3])<<8 | uint32(packet[4])
|
||||||
|
|
||||||
|
s.lock.Lock()
|
||||||
|
c, ok := s.channels[chanId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.handleData(packet[9:])
|
||||||
|
s.lock.Unlock()
|
||||||
|
|
||||||
|
case msgChannelEOF:
|
||||||
|
var eofMsg channelEOFMsg
|
||||||
|
if err := unmarshal(&eofMsg, packet, msgChannelEOF); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lock.Lock()
|
||||||
|
c, ok := s.channels[eofMsg.PeersId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.handlePacket(&eofMsg)
|
||||||
|
s.lock.Unlock()
|
||||||
|
|
||||||
|
case msgChannelClose:
|
||||||
|
var closeMsg channelCloseMsg
|
||||||
|
if err := unmarshal(&closeMsg, packet, msgChannelClose); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lock.Lock()
|
||||||
|
c, ok := s.channels[closeMsg.PeersId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.handlePacket(&closeMsg)
|
||||||
|
s.lock.Unlock()
|
||||||
|
|
||||||
|
case msgGlobalRequest:
|
||||||
|
var request globalRequestMsg
|
||||||
|
if err := unmarshal(&request, packet, msgGlobalRequest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.WantReply {
|
||||||
|
if err := s.out.writePacket([]byte{msgRequestFailure}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unknown message. Ignore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
399
src/pkg/exp/ssh/server_shell.go
Normal file
399
src/pkg/exp/ssh/server_shell.go
Normal file
@ -0,0 +1,399 @@
|
|||||||
|
// 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 (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServerShell contains the state for running a VT100 terminal that is capable
|
||||||
|
// of reading lines of input.
|
||||||
|
type ServerShell struct {
|
||||||
|
c Channel
|
||||||
|
prompt string
|
||||||
|
|
||||||
|
// line is the current line being entered.
|
||||||
|
line []byte
|
||||||
|
// pos is the logical position of the cursor in line
|
||||||
|
pos int
|
||||||
|
|
||||||
|
// cursorX contains the current X value of the cursor where the left
|
||||||
|
// edge is 0. cursorY contains the row number where the first row of
|
||||||
|
// the current line is 0.
|
||||||
|
cursorX, cursorY int
|
||||||
|
// maxLine is the greatest value of cursorY so far.
|
||||||
|
maxLine int
|
||||||
|
|
||||||
|
termWidth, termHeight int
|
||||||
|
|
||||||
|
// outBuf contains the terminal data to be sent.
|
||||||
|
outBuf []byte
|
||||||
|
// remainder contains the remainder of any partial key sequences after
|
||||||
|
// a read. It aliases into inBuf.
|
||||||
|
remainder []byte
|
||||||
|
inBuf [256]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServerShell runs a VT100 terminal on the given channel. prompt is a
|
||||||
|
// string that is written at the start of each input line. For example: "> ".
|
||||||
|
func NewServerShell(c Channel, prompt string) *ServerShell {
|
||||||
|
return &ServerShell{
|
||||||
|
c: c,
|
||||||
|
prompt: prompt,
|
||||||
|
termWidth: 80,
|
||||||
|
termHeight: 24,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyCtrlD = 4
|
||||||
|
keyEnter = '\r'
|
||||||
|
keyEscape = 27
|
||||||
|
keyBackspace = 127
|
||||||
|
keyUnknown = 256 + iota
|
||||||
|
keyUp
|
||||||
|
keyDown
|
||||||
|
keyLeft
|
||||||
|
keyRight
|
||||||
|
keyAltLeft
|
||||||
|
keyAltRight
|
||||||
|
)
|
||||||
|
|
||||||
|
// bytesToKey tries to parse a key sequence from b. If successful, it returns
|
||||||
|
// the key and the remainder of the input. Otherwise it returns -1.
|
||||||
|
func bytesToKey(b []byte) (int, []byte) {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if b[0] != keyEscape {
|
||||||
|
return int(b[0]), b[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) >= 3 && b[0] == keyEscape && b[1] == '[' {
|
||||||
|
switch b[2] {
|
||||||
|
case 'A':
|
||||||
|
return keyUp, b[3:]
|
||||||
|
case 'B':
|
||||||
|
return keyDown, b[3:]
|
||||||
|
case 'C':
|
||||||
|
return keyRight, b[3:]
|
||||||
|
case 'D':
|
||||||
|
return keyLeft, b[3:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' {
|
||||||
|
switch b[5] {
|
||||||
|
case 'C':
|
||||||
|
return keyAltRight, b[6:]
|
||||||
|
case 'D':
|
||||||
|
return keyAltLeft, b[6:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here then we have a key that we don't recognise, or a
|
||||||
|
// partial sequence. It's not clear how one should find the end of a
|
||||||
|
// sequence without knowing them all, but it seems that [a-zA-Z] only
|
||||||
|
// appears at the end of a sequence.
|
||||||
|
for i, c := range b[0:] {
|
||||||
|
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' {
|
||||||
|
return keyUnknown, b[i+1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1, b
|
||||||
|
}
|
||||||
|
|
||||||
|
// queue appends data to the end of ss.outBuf
|
||||||
|
func (ss *ServerShell) queue(data []byte) {
|
||||||
|
if len(ss.outBuf)+len(data) > cap(ss.outBuf) {
|
||||||
|
newOutBuf := make([]byte, len(ss.outBuf), 2*(len(ss.outBuf)+len(data)))
|
||||||
|
copy(newOutBuf, ss.outBuf)
|
||||||
|
ss.outBuf = newOutBuf
|
||||||
|
}
|
||||||
|
|
||||||
|
oldLen := len(ss.outBuf)
|
||||||
|
ss.outBuf = ss.outBuf[:len(ss.outBuf)+len(data)]
|
||||||
|
copy(ss.outBuf[oldLen:], data)
|
||||||
|
}
|
||||||
|
|
||||||
|
var eraseUnderCursor = []byte{' ', keyEscape, '[', 'D'}
|
||||||
|
|
||||||
|
func isPrintable(key int) bool {
|
||||||
|
return key >= 32 && key < 127
|
||||||
|
}
|
||||||
|
|
||||||
|
// moveCursorToPos appends data to ss.outBuf which will move the cursor to the
|
||||||
|
// given, logical position in the text.
|
||||||
|
func (ss *ServerShell) moveCursorToPos(pos int) {
|
||||||
|
x := len(ss.prompt) + pos
|
||||||
|
y := x / ss.termWidth
|
||||||
|
x = x % ss.termWidth
|
||||||
|
|
||||||
|
up := 0
|
||||||
|
if y < ss.cursorY {
|
||||||
|
up = ss.cursorY - y
|
||||||
|
}
|
||||||
|
|
||||||
|
down := 0
|
||||||
|
if y > ss.cursorY {
|
||||||
|
down = y - ss.cursorY
|
||||||
|
}
|
||||||
|
|
||||||
|
left := 0
|
||||||
|
if x < ss.cursorX {
|
||||||
|
left = ss.cursorX - x
|
||||||
|
}
|
||||||
|
|
||||||
|
right := 0
|
||||||
|
if x > ss.cursorX {
|
||||||
|
right = x - ss.cursorX
|
||||||
|
}
|
||||||
|
|
||||||
|
movement := make([]byte, 3*(up+down+left+right))
|
||||||
|
m := movement
|
||||||
|
for i := 0; i < up; i++ {
|
||||||
|
m[0] = keyEscape
|
||||||
|
m[1] = '['
|
||||||
|
m[2] = 'A'
|
||||||
|
m = m[3:]
|
||||||
|
}
|
||||||
|
for i := 0; i < down; i++ {
|
||||||
|
m[0] = keyEscape
|
||||||
|
m[1] = '['
|
||||||
|
m[2] = 'B'
|
||||||
|
m = m[3:]
|
||||||
|
}
|
||||||
|
for i := 0; i < left; i++ {
|
||||||
|
m[0] = keyEscape
|
||||||
|
m[1] = '['
|
||||||
|
m[2] = 'D'
|
||||||
|
m = m[3:]
|
||||||
|
}
|
||||||
|
for i := 0; i < right; i++ {
|
||||||
|
m[0] = keyEscape
|
||||||
|
m[1] = '['
|
||||||
|
m[2] = 'C'
|
||||||
|
m = m[3:]
|
||||||
|
}
|
||||||
|
|
||||||
|
ss.cursorX = x
|
||||||
|
ss.cursorY = y
|
||||||
|
ss.queue(movement)
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxLineLength = 4096
|
||||||
|
|
||||||
|
// handleKey processes the given key and, optionally, returns a line of text
|
||||||
|
// that the user has entered.
|
||||||
|
func (ss *ServerShell) handleKey(key int) (line string, ok bool) {
|
||||||
|
switch key {
|
||||||
|
case keyBackspace:
|
||||||
|
if ss.pos == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ss.pos--
|
||||||
|
|
||||||
|
copy(ss.line[ss.pos:], ss.line[1+ss.pos:])
|
||||||
|
ss.line = ss.line[:len(ss.line)-1]
|
||||||
|
ss.writeLine(ss.line[ss.pos:])
|
||||||
|
ss.moveCursorToPos(ss.pos)
|
||||||
|
ss.queue(eraseUnderCursor)
|
||||||
|
case keyAltLeft:
|
||||||
|
// move left by a word.
|
||||||
|
if ss.pos == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ss.pos--
|
||||||
|
for ss.pos > 0 {
|
||||||
|
if ss.line[ss.pos] != ' ' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ss.pos--
|
||||||
|
}
|
||||||
|
for ss.pos > 0 {
|
||||||
|
if ss.line[ss.pos] == ' ' {
|
||||||
|
ss.pos++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ss.pos--
|
||||||
|
}
|
||||||
|
ss.moveCursorToPos(ss.pos)
|
||||||
|
case keyAltRight:
|
||||||
|
// move right by a word.
|
||||||
|
for ss.pos < len(ss.line) {
|
||||||
|
if ss.line[ss.pos] == ' ' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ss.pos++
|
||||||
|
}
|
||||||
|
for ss.pos < len(ss.line) {
|
||||||
|
if ss.line[ss.pos] != ' ' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ss.pos++
|
||||||
|
}
|
||||||
|
ss.moveCursorToPos(ss.pos)
|
||||||
|
case keyLeft:
|
||||||
|
if ss.pos == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ss.pos--
|
||||||
|
ss.moveCursorToPos(ss.pos)
|
||||||
|
case keyRight:
|
||||||
|
if ss.pos == len(ss.line) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ss.pos++
|
||||||
|
ss.moveCursorToPos(ss.pos)
|
||||||
|
case keyEnter:
|
||||||
|
ss.moveCursorToPos(len(ss.line))
|
||||||
|
ss.queue([]byte("\r\n"))
|
||||||
|
line = string(ss.line)
|
||||||
|
ok = true
|
||||||
|
ss.line = ss.line[:0]
|
||||||
|
ss.pos = 0
|
||||||
|
ss.cursorX = 0
|
||||||
|
ss.cursorY = 0
|
||||||
|
ss.maxLine = 0
|
||||||
|
default:
|
||||||
|
if !isPrintable(key) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(ss.line) == maxLineLength {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(ss.line) == cap(ss.line) {
|
||||||
|
newLine := make([]byte, len(ss.line), 2*(1+len(ss.line)))
|
||||||
|
copy(newLine, ss.line)
|
||||||
|
ss.line = newLine
|
||||||
|
}
|
||||||
|
ss.line = ss.line[:len(ss.line)+1]
|
||||||
|
copy(ss.line[ss.pos+1:], ss.line[ss.pos:])
|
||||||
|
ss.line[ss.pos] = byte(key)
|
||||||
|
ss.writeLine(ss.line[ss.pos:])
|
||||||
|
ss.pos++
|
||||||
|
ss.moveCursorToPos(ss.pos)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *ServerShell) writeLine(line []byte) {
|
||||||
|
for len(line) != 0 {
|
||||||
|
if ss.cursorX == ss.termWidth {
|
||||||
|
ss.queue([]byte("\r\n"))
|
||||||
|
ss.cursorX = 0
|
||||||
|
ss.cursorY++
|
||||||
|
if ss.cursorY > ss.maxLine {
|
||||||
|
ss.maxLine = ss.cursorY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingOnLine := ss.termWidth - ss.cursorX
|
||||||
|
todo := len(line)
|
||||||
|
if todo > remainingOnLine {
|
||||||
|
todo = remainingOnLine
|
||||||
|
}
|
||||||
|
ss.queue(line[:todo])
|
||||||
|
ss.cursorX += todo
|
||||||
|
line = line[todo:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 *ServerShell) Write(buf []byte) (n int, err os.Error) {
|
||||||
|
return ss.c.Write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadLine returns a line of input from the terminal.
|
||||||
|
func (ss *ServerShell) ReadLine() (line string, err os.Error) {
|
||||||
|
ss.writeLine([]byte(ss.prompt))
|
||||||
|
ss.c.Write(ss.outBuf)
|
||||||
|
ss.outBuf = ss.outBuf[:0]
|
||||||
|
|
||||||
|
for {
|
||||||
|
// ss.remainder is a slice at the beginning of ss.inBuf
|
||||||
|
// containing a partial key sequence
|
||||||
|
readBuf := ss.inBuf[len(ss.remainder):]
|
||||||
|
n, err := ss.c.Read(readBuf)
|
||||||
|
if err == nil {
|
||||||
|
ss.remainder = ss.inBuf[:n+len(ss.remainder)]
|
||||||
|
rest := ss.remainder
|
||||||
|
lineOk := false
|
||||||
|
for !lineOk {
|
||||||
|
var key int
|
||||||
|
key, rest = bytesToKey(rest)
|
||||||
|
if key < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if key == keyCtrlD {
|
||||||
|
return "", os.EOF
|
||||||
|
}
|
||||||
|
line, lineOk = ss.handleKey(key)
|
||||||
|
}
|
||||||
|
if len(rest) > 0 {
|
||||||
|
n := copy(ss.inBuf[:], rest)
|
||||||
|
ss.remainder = ss.inBuf[:n]
|
||||||
|
} else {
|
||||||
|
ss.remainder = nil
|
||||||
|
}
|
||||||
|
ss.c.Write(ss.outBuf)
|
||||||
|
ss.outBuf = ss.outBuf[:0]
|
||||||
|
if lineOk {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if req, ok := err.(ChannelRequest); ok {
|
||||||
|
ok := false
|
||||||
|
switch req.Request {
|
||||||
|
case "pty-req":
|
||||||
|
ss.termWidth, ss.termHeight, ok = parsePtyRequest(req.Payload)
|
||||||
|
if !ok {
|
||||||
|
ss.termWidth = 80
|
||||||
|
ss.termHeight = 24
|
||||||
|
}
|
||||||
|
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.c.AckRequest(ok)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
134
src/pkg/exp/ssh/server_shell_test.go
Normal file
134
src/pkg/exp/ssh/server_shell_test.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
// 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"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockChannel struct {
|
||||||
|
toSend []byte
|
||||||
|
bytesPerRead int
|
||||||
|
received []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockChannel) Accept() os.Error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockChannel) Reject(RejectionReason, string) os.Error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockChannel) Read(data []byte) (n int, err os.Error) {
|
||||||
|
n = len(data)
|
||||||
|
if n == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n > len(c.toSend) {
|
||||||
|
n = len(c.toSend)
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
return 0, os.EOF
|
||||||
|
}
|
||||||
|
if c.bytesPerRead > 0 && n > c.bytesPerRead {
|
||||||
|
n = c.bytesPerRead
|
||||||
|
}
|
||||||
|
copy(data, c.toSend[:n])
|
||||||
|
c.toSend = c.toSend[n:]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockChannel) Write(data []byte) (n int, err os.Error) {
|
||||||
|
c.received = append(c.received, data...)
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockChannel) Close() os.Error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockChannel) AckRequest(ok bool) os.Error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockChannel) ChannelType() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MockChannel) ExtraData() []byte {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClose(t *testing.T) {
|
||||||
|
c := &MockChannel{}
|
||||||
|
ss := NewServerShell(c, "> ")
|
||||||
|
line, err := ss.ReadLine()
|
||||||
|
if line != "" {
|
||||||
|
t.Errorf("Expected empty line but got: %s", line)
|
||||||
|
}
|
||||||
|
if err != os.EOF {
|
||||||
|
t.Errorf("Error should have been EOF but got: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyPressTests = []struct {
|
||||||
|
in string
|
||||||
|
line string
|
||||||
|
err os.Error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
os.EOF,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"\r",
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo\r",
|
||||||
|
"foo",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a\x1b[Cb\r", // right
|
||||||
|
"ab",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a\x1b[Db\r", // left
|
||||||
|
"ba",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a\177b\r", // backspace
|
||||||
|
"b",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyPresses(t *testing.T) {
|
||||||
|
for i, test := range keyPressTests {
|
||||||
|
for j := 0; j < len(test.in); j++ {
|
||||||
|
c := &MockChannel{
|
||||||
|
toSend: []byte(test.in),
|
||||||
|
bytesPerRead: j,
|
||||||
|
}
|
||||||
|
ss := NewServerShell(c, "> ")
|
||||||
|
line, err := ss.ReadLine()
|
||||||
|
if line != test.line {
|
||||||
|
t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != test.err {
|
||||||
|
t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
308
src/pkg/exp/ssh/transport.go
Normal file
308
src/pkg/exp/ssh/transport.go
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
// 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/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/subtle"
|
||||||
|
"hash"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// halfConnection represents one direction of an SSH connection. It maintains
|
||||||
|
// the cipher state needed to process messages.
|
||||||
|
type halfConnection struct {
|
||||||
|
// Only one of these two will be non-nil
|
||||||
|
in *bufio.Reader
|
||||||
|
out net.Conn
|
||||||
|
|
||||||
|
rand io.Reader
|
||||||
|
cipherAlgo string
|
||||||
|
macAlgo string
|
||||||
|
compressionAlgo string
|
||||||
|
paddingMultiple int
|
||||||
|
|
||||||
|
seqNum uint32
|
||||||
|
|
||||||
|
mac hash.Hash
|
||||||
|
cipher cipher.Stream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *halfConnection) readOnePacket() (packet []byte, err os.Error) {
|
||||||
|
var lengthBytes [5]byte
|
||||||
|
|
||||||
|
_, err = io.ReadFull(hc.in, lengthBytes[:])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if hc.cipher != nil {
|
||||||
|
hc.cipher.XORKeyStream(lengthBytes[:], lengthBytes[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
macSize := 0
|
||||||
|
if hc.mac != nil {
|
||||||
|
hc.mac.Reset()
|
||||||
|
var seqNumBytes [4]byte
|
||||||
|
seqNumBytes[0] = byte(hc.seqNum >> 24)
|
||||||
|
seqNumBytes[1] = byte(hc.seqNum >> 16)
|
||||||
|
seqNumBytes[2] = byte(hc.seqNum >> 8)
|
||||||
|
seqNumBytes[3] = byte(hc.seqNum)
|
||||||
|
hc.mac.Write(seqNumBytes[:])
|
||||||
|
hc.mac.Write(lengthBytes[:])
|
||||||
|
macSize = hc.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, os.NewError("invalid packet length")
|
||||||
|
}
|
||||||
|
if length > maxPacketSize {
|
||||||
|
return nil, os.NewError("packet too large")
|
||||||
|
}
|
||||||
|
|
||||||
|
packet = make([]byte, length-1+uint32(macSize))
|
||||||
|
_, err = io.ReadFull(hc.in, packet)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mac := packet[length-1:]
|
||||||
|
if hc.cipher != nil {
|
||||||
|
hc.cipher.XORKeyStream(packet, packet[:length-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
if hc.mac != nil {
|
||||||
|
hc.mac.Write(packet[:length-1])
|
||||||
|
if subtle.ConstantTimeCompare(hc.mac.Sum(), mac) != 1 {
|
||||||
|
return nil, os.NewError("ssh: MAC failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hc.seqNum++
|
||||||
|
packet = packet[:length-paddingLength-1]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *halfConnection) readPacket() (packet []byte, err os.Error) {
|
||||||
|
for {
|
||||||
|
packet, err := hc.readOnePacket()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if packet[0] != msgIgnore && packet[0] != msgDebug {
|
||||||
|
return packet, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *halfConnection) writePacket(packet []byte) os.Error {
|
||||||
|
paddingMultiple := hc.paddingMultiple
|
||||||
|
if paddingMultiple == 0 {
|
||||||
|
paddingMultiple = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
paddingLength := paddingMultiple - (4+1+len(packet))%paddingMultiple
|
||||||
|
if paddingLength < 4 {
|
||||||
|
paddingLength += paddingMultiple
|
||||||
|
}
|
||||||
|
|
||||||
|
var lengthBytes [5]byte
|
||||||
|
length := len(packet) + 1 + paddingLength
|
||||||
|
lengthBytes[0] = byte(length >> 24)
|
||||||
|
lengthBytes[1] = byte(length >> 16)
|
||||||
|
lengthBytes[2] = byte(length >> 8)
|
||||||
|
lengthBytes[3] = byte(length)
|
||||||
|
lengthBytes[4] = byte(paddingLength)
|
||||||
|
|
||||||
|
var padding [32]byte
|
||||||
|
_, err := io.ReadFull(hc.rand, padding[:paddingLength])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hc.mac != nil {
|
||||||
|
hc.mac.Reset()
|
||||||
|
var seqNumBytes [4]byte
|
||||||
|
seqNumBytes[0] = byte(hc.seqNum >> 24)
|
||||||
|
seqNumBytes[1] = byte(hc.seqNum >> 16)
|
||||||
|
seqNumBytes[2] = byte(hc.seqNum >> 8)
|
||||||
|
seqNumBytes[3] = byte(hc.seqNum)
|
||||||
|
hc.mac.Write(seqNumBytes[:])
|
||||||
|
hc.mac.Write(lengthBytes[:])
|
||||||
|
hc.mac.Write(packet)
|
||||||
|
hc.mac.Write(padding[:paddingLength])
|
||||||
|
}
|
||||||
|
|
||||||
|
if hc.cipher != nil {
|
||||||
|
hc.cipher.XORKeyStream(lengthBytes[:], lengthBytes[:])
|
||||||
|
hc.cipher.XORKeyStream(packet, packet)
|
||||||
|
hc.cipher.XORKeyStream(padding[:], padding[:paddingLength])
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = hc.out.Write(lengthBytes[:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = hc.out.Write(packet)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = hc.out.Write(padding[:paddingLength])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hc.mac != nil {
|
||||||
|
_, err = hc.out.Write(hc.mac.Sum())
|
||||||
|
}
|
||||||
|
|
||||||
|
hc.seqNum++
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
serverKeys = iota
|
||||||
|
clientKeys
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupServerKeys sets the cipher and MAC keys from K, 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 (hc *halfConnection) setupKeys(direction int, K, H, sessionId []byte, hashFunc crypto.Hash) os.Error {
|
||||||
|
h := hashFunc.New()
|
||||||
|
|
||||||
|
// We only support these algorithms for now.
|
||||||
|
if hc.cipherAlgo != cipherAES128CTR || hc.macAlgo != macSHA196 {
|
||||||
|
return os.NewError("ssh: setupServerKeys internal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
blockSize := 16
|
||||||
|
keySize := 16
|
||||||
|
macKeySize := 20
|
||||||
|
|
||||||
|
var ivTag, keyTag, macKeyTag byte
|
||||||
|
if direction == serverKeys {
|
||||||
|
ivTag, keyTag, macKeyTag = 'B', 'D', 'F'
|
||||||
|
} else {
|
||||||
|
ivTag, keyTag, macKeyTag = 'A', 'C', 'E'
|
||||||
|
}
|
||||||
|
|
||||||
|
iv := make([]byte, blockSize)
|
||||||
|
key := make([]byte, keySize)
|
||||||
|
macKey := make([]byte, macKeySize)
|
||||||
|
generateKeyMaterial(iv, ivTag, K, H, sessionId, h)
|
||||||
|
generateKeyMaterial(key, keyTag, K, H, sessionId, h)
|
||||||
|
generateKeyMaterial(macKey, macKeyTag, K, H, sessionId, h)
|
||||||
|
|
||||||
|
hc.mac = truncatingMAC{12, hmac.NewSHA1(macKey)}
|
||||||
|
aes, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hc.cipher = cipher.NewCTR(aes, iv)
|
||||||
|
hc.paddingMultiple = 16
|
||||||
|
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 []byte, 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([]byte{tag})
|
||||||
|
h.Write(sessionId)
|
||||||
|
} else {
|
||||||
|
h.Write(digestsSoFar)
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := h.Sum()
|
||||||
|
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, os.Error) {
|
||||||
|
return t.hmac.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t truncatingMAC) Sum() []byte {
|
||||||
|
digest := t.hmac.Sum()
|
||||||
|
return digest[:t.length]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t truncatingMAC) Reset() {
|
||||||
|
t.hmac.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t truncatingMAC) Size() int {
|
||||||
|
return t.length
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
func readVersion(r *bufio.Reader) (versionString []byte, ok bool) {
|
||||||
|
versionString = make([]byte, 0, 64)
|
||||||
|
seenCR := false
|
||||||
|
|
||||||
|
forEachByte:
|
||||||
|
for len(versionString) < maxVersionStringBytes {
|
||||||
|
b, err := r.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !seenCR {
|
||||||
|
if b == '\r' {
|
||||||
|
seenCR = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if b == '\n' {
|
||||||
|
ok = true
|
||||||
|
break forEachByte
|
||||||
|
} else {
|
||||||
|
seenCR = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
versionString = append(versionString, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
// We need to remove the CR from versionString
|
||||||
|
versionString = versionString[:len(versionString)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user