Commit 57912334 authored by Dave Cheney's avatar Dave Cheney Committed by Adam Langley

exp/ssh: introduce Session to replace Cmd for interactive commands

This CL replaces the Cmd type with a Session type representing
interactive channels. This lays the foundation for supporting
other kinds of channels like direct-tcpip or x11.

client.go:
* replace chanlist map with slice.
* generalize stdout and stderr into a single type.
* unexport ClientChan to clientChan.

doc.go:
* update ServerConfig/ServerConn documentation.
* update Client example for Session.

message.go:
* make channelExtendedData more like channelData.

session.go:
* added Session which replaces Cmd.

R=agl, rsc, n13m3y3r, gustavo
CC=golang-dev
https://golang.org/cl/5302054
parent 2f3f3aa2
...@@ -13,5 +13,6 @@ GOFILES=\ ...@@ -13,5 +13,6 @@ GOFILES=\
transport.go\ transport.go\
server.go\ server.go\
server_shell.go\ server_shell.go\
session.go\
include ../../../Make.pkg include ../../../Make.pkg
This diff is collapsed.
...@@ -11,26 +11,29 @@ protocol is a remote shell and this is specifically implemented. However, ...@@ -11,26 +11,29 @@ protocol is a remote shell and this is specifically implemented. However,
the multiplexed nature of SSH is exposed to users that wish to support the multiplexed nature of SSH is exposed to users that wish to support
others. others.
An SSH server is represented by a Server, which manages a number of An SSH server is represented by a ServerConfig, which holds certificate
ServerConnections and handles authentication. details and handles authentication of ServerConns.
var s Server config := new(ServerConfig)
s.PubKeyCallback = pubKeyAuth config.PubKeyCallback = pubKeyAuth
s.PasswordCallback = passwordAuth config.PasswordCallback = passwordAuth
pemBytes, err := ioutil.ReadFile("id_rsa") pemBytes, err := ioutil.ReadFile("id_rsa")
if err != nil { if err != nil {
panic("Failed to load private key") panic("Failed to load private key")
} }
err = s.SetRSAPrivateKey(pemBytes) err = config.SetRSAPrivateKey(pemBytes)
if err != nil { if err != nil {
panic("Failed to parse private key") panic("Failed to parse private key")
} }
Once a Server has been set up, connections can be attached. Once a ServerConfig has been configured, connections can be accepted.
var sConn ServerConnection listener := Listen("tcp", "0.0.0.0:2022", config)
sConn.Server = &s sConn, err := listener.Accept()
if err != nil {
panic("failed to accept incoming connection")
}
err = sConn.Handshake(conn) err = sConn.Handshake(conn)
if err != nil { if err != nil {
panic("failed to handshake") panic("failed to handshake")
...@@ -38,7 +41,6 @@ Once a Server has been set up, connections can be attached. ...@@ -38,7 +41,6 @@ Once a Server has been set up, connections can be attached.
An SSH connection multiplexes several channels, which must be accepted themselves: An SSH connection multiplexes several channels, which must be accepted themselves:
for { for {
channel, err := sConn.Accept() channel, err := sConn.Accept()
if err != nil { if err != nil {
...@@ -85,17 +87,19 @@ authentication method is supported. ...@@ -85,17 +87,19 @@ authentication method is supported.
} }
client, err := Dial("yourserver.com:22", config) client, err := Dial("yourserver.com:22", config)
Each ClientConn can support multiple channels, represented by ClientChan. Each Each ClientConn can support multiple interactive sessions, represented by a Session.
channel should be of a type specified in rfc4250, 4.9.1.
ch, err := client.OpenChan("session") session, err := client.NewSession()
Once the ClientChan is opened, you can execute a single command on the remote side Once a Session is created, you can execute a single command on the remote side
using the Exec method. using the Exec method.
cmd, err := ch.Exec("/usr/bin/whoami") if err := session.Exec("/usr/bin/whoami"); err != nil {
reader := bufio.NewReader(cmd.Stdin) panic("Failed to exec: " + err.String())
}
reader := bufio.NewReader(session.Stdin)
line, _, _ := reader.ReadLine() line, _, _ := reader.ReadLine()
fmt.Println(line) fmt.Println(line)
session.Close()
*/ */
package ssh package ssh
...@@ -154,7 +154,7 @@ type channelData struct { ...@@ -154,7 +154,7 @@ type channelData struct {
type channelExtendedData struct { type channelExtendedData struct {
PeersId uint32 PeersId uint32
Datatype uint32 Datatype uint32
Data string Payload []byte `ssh:"rest"`
} }
type channelRequestMsg struct { type channelRequestMsg struct {
......
// 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 (
"encoding/binary"
"io"
"os"
)
// A Session represents a connection to a remote command or shell.
type Session struct {
// Writes to Stdin are made available to the remote command's standard input.
// Closing Stdin causes the command to observe an EOF on its standard input.
Stdin io.WriteCloser
// Reads from Stdout and Stderr consume from the remote command's standard
// output and error streams, respectively.
// There is a fixed amount of buffering that is shared for the two streams.
// Failing to read from either may eventually cause the command to block.
// Closing Stdout unblocks such writes and causes them to return errors.
Stdout io.ReadCloser
Stderr io.Reader
*clientChan // the channel backing this session
started bool // started is set to true once a Shell or Exec is invoked.
}
// Setenv sets an environment variable that will be applied to any
// command executed by Shell or Exec.
func (s *Session) Setenv(name, value string) os.Error {
n, v := []byte(name), []byte(value)
nlen, vlen := stringLength(n), stringLength(v)
payload := make([]byte, nlen+vlen)
marshalString(payload[:nlen], n)
marshalString(payload[nlen:], v)
return s.sendChanReq(channelRequestMsg{
PeersId: s.id,
Request: "env",
WantReply: true,
RequestSpecificData: payload,
})
}
// An empty mode list (a string of 1 character, opcode 0), see RFC 4254 Section 8.
var emptyModeList = []byte{0, 0, 0, 1, 0}
// RequestPty requests the association of a pty with the session on the remote host.
func (s *Session) RequestPty(term string, h, w int) os.Error {
buf := make([]byte, 4+len(term)+16+len(emptyModeList))
b := marshalString(buf, []byte(term))
binary.BigEndian.PutUint32(b, uint32(h))
binary.BigEndian.PutUint32(b[4:], uint32(w))
binary.BigEndian.PutUint32(b[8:], uint32(h*8))
binary.BigEndian.PutUint32(b[12:], uint32(w*8))
copy(b[16:], emptyModeList)
return s.sendChanReq(channelRequestMsg{
PeersId: s.id,
Request: "pty-req",
WantReply: true,
RequestSpecificData: buf,
})
}
// Exec runs cmd on the remote host. Typically, the remote
// server passes cmd to the shell for interpretation.
// A Session only accepts one call to Exec or Shell.
func (s *Session) Exec(cmd string) os.Error {
if s.started {
return os.NewError("session already started")
}
cmdLen := stringLength([]byte(cmd))
payload := make([]byte, cmdLen)
marshalString(payload, []byte(cmd))
s.started = true
return s.sendChanReq(channelRequestMsg{
PeersId: s.id,
Request: "exec",
WantReply: true,
RequestSpecificData: payload,
})
}
// Shell starts a login shell on the remote host. A Session only
// accepts one call to Exec or Shell.
func (s *Session) Shell() os.Error {
if s.started {
return os.NewError("session already started")
}
s.started = true
return s.sendChanReq(channelRequestMsg{
PeersId: s.id,
Request: "shell",
WantReply: true,
})
}
// NewSession returns a new interactive session on the remote host.
func (c *ClientConn) NewSession() (*Session, os.Error) {
ch, err := c.openChan("session")
if err != nil {
return nil, err
}
return &Session{
Stdin: &chanWriter{
packetWriter: ch,
id: ch.id,
win: ch.win,
},
Stdout: &chanReader{
packetWriter: ch,
id: ch.id,
data: ch.data,
},
Stderr: &chanReader{
packetWriter: ch,
id: ch.id,
data: ch.dataExt,
},
clientChan: ch,
}, nil
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment