feat(bsr/ssh): Add SSH Request type chunks
This commit is contained in:
1 parent
26bd86fb07
commit
ba476ca73b
46 files changed
+6267
-1
No files matched your search
@@ -142,12 +142,13 @@ proto: protolint protobuild
|
||||
|
||||
.PHONY: protobuild
|
||||
protobuild:
|
||||
@buf generate -o "${TMP_DIR}"
|
||||
@buf generate -o "${TMP_DIR}" internal/proto
|
||||
|
||||
# Move the generated files from the tmp file subdirectories into the current repo.
|
||||
cp -R ${TMP_DIR}/${REPO_PATH}/* ${THIS_DIR}
|
||||
|
||||
@buf generate --template buf.openapiv2.gen.yaml --path internal/proto/controller/api/services/v1/
|
||||
cd internal/bsr/ && buf generate proto/
|
||||
|
||||
@protoc-go-inject-tag -input=./internal/oplog/store/oplog.pb.go
|
||||
@protoc-go-inject-tag -input=./internal/oplog/oplog_test/oplog_test.pb.go
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
version: v1
|
||||
directories:
|
||||
- internal/proto
|
||||
- internal/bsr/proto
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
version: v1
|
||||
managed:
|
||||
enabled: true
|
||||
go_package_prefix:
|
||||
default: github.com/hashicorp/boundary/internal/bsr/gen
|
||||
plugins:
|
||||
- name: go
|
||||
out: gen
|
||||
opt:
|
||||
- paths=source_relative
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,7 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
version: v1
|
||||
lint:
|
||||
use:
|
||||
- DEFAULT
|
||||
@@ -0,0 +1,368 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package ssh.v1;
|
||||
|
||||
// An UnknownRequest is an unrecognized SSH request. We'll record the type, want_reply, and payload
|
||||
message UnknownRequest {
|
||||
// The request type field
|
||||
string request_type = 1;
|
||||
|
||||
// The want_reply field on the SSH request
|
||||
bool want_reply = 2;
|
||||
|
||||
// The Payload field on the SSH request
|
||||
bytes data = 3;
|
||||
}
|
||||
|
||||
// Fields for the following SSH request protos were taken from
|
||||
// https://www.iana.org/assignments/ssh-parameters/ssh-parameters.xhtml#ssh-parameters-11
|
||||
|
||||
// A session is started by sending the following message.
|
||||
message SessionRequest {
|
||||
// The request type field. This should always be "session"
|
||||
string request_type = 1;
|
||||
|
||||
// The sender channel
|
||||
uint32 sender_channel = 2;
|
||||
|
||||
// The initial window size
|
||||
uint32 initial_window_size = 3;
|
||||
|
||||
// The maximum packet size
|
||||
uint32 maximum_packet_size = 4;
|
||||
}
|
||||
|
||||
// X11 channels are opened with a channel open request. The resulting
|
||||
// channels are independent of the session, and closing the session
|
||||
// channel does not close the forwarded X11 channels.
|
||||
message X11Request {
|
||||
// The request type field. This should always be "x11"
|
||||
string request_type = 1;
|
||||
|
||||
// The sender channel
|
||||
uint32 sender_channel = 2;
|
||||
|
||||
// The initial window size
|
||||
uint32 initial_window_size = 3;
|
||||
|
||||
// The maximum packet size
|
||||
uint32 maximum_packet_size = 4;
|
||||
|
||||
// The originator address (e.g., "192.168.7.38")
|
||||
string originator_address = 5;
|
||||
|
||||
// The originator port
|
||||
uint32 originator_port = 6;
|
||||
}
|
||||
|
||||
// When a connection comes to a port for which remote forwarding has
|
||||
// been requested, a channel is opened to forward the port to the other
|
||||
// side.
|
||||
message ForwardedTCPIPRequest {
|
||||
// The request type field. This should always be "forwarded-tcpip"
|
||||
string request_type = 1;
|
||||
|
||||
// The sender channel
|
||||
uint32 sender_channel = 2;
|
||||
|
||||
// The initial window size
|
||||
uint32 initial_window_size = 3;
|
||||
|
||||
// The maximum packet size
|
||||
uint32 maximum_packet_size = 4;
|
||||
|
||||
// The address that was connected
|
||||
string address = 5;
|
||||
|
||||
// The port that was connected
|
||||
uint32 port = 6;
|
||||
|
||||
// The originator ip address (e.g., "192.168.7.38")
|
||||
string originator_ip_address = 7;
|
||||
|
||||
// The originator port
|
||||
uint32 originator_port = 8;
|
||||
}
|
||||
|
||||
// When a connection comes to a locally forwarded TCP/IP port, the
|
||||
// following packet is sent to the other side.
|
||||
message DirectTCPIPRequest {
|
||||
// The request type field. This should always be "direct-tcpip"
|
||||
string request_type = 1;
|
||||
|
||||
// The sender channel
|
||||
uint32 sender_channel = 2;
|
||||
|
||||
// The initial window size
|
||||
uint32 initial_window_size = 3;
|
||||
|
||||
// The maximum packet size
|
||||
uint32 maximum_packet_size = 4;
|
||||
|
||||
// The host to connect
|
||||
string host = 5;
|
||||
|
||||
// The port to connect
|
||||
uint32 port = 6;
|
||||
|
||||
// The 'originator IP address' is the numeric IP address of the machine
|
||||
// from where the connection request originates
|
||||
string originator_ip_address = 7;
|
||||
|
||||
// The port on the host from where the connection originated.
|
||||
uint32 originator_port = 8;
|
||||
}
|
||||
|
||||
// Fields for the following SSH request protos were taken from
|
||||
// https://www.iana.org/assignments/ssh-parameters/ssh-parameters.xhtml#ssh-parameters-13
|
||||
|
||||
// A pseudo-terminal can be allocated for the session by sending the
|
||||
// following message.
|
||||
message PtyRequest {
|
||||
// The request type field. This should always be "pty-req"
|
||||
string request_type = 1;
|
||||
|
||||
// The want_reply field on the SSH request
|
||||
bool want_reply = 2;
|
||||
|
||||
// TERM environment variable value (e.g., vt100)
|
||||
string term_env_var = 3;
|
||||
|
||||
// Terminal width, characters (e.g., 80)
|
||||
uint32 terminal_width_characters = 4;
|
||||
|
||||
// Terminal height, rows (e.g., 24)
|
||||
uint32 terminal_height_rows = 5;
|
||||
|
||||
// Terminal width, pixels (e.g., 640)
|
||||
uint32 terminal_width_pixels = 6;
|
||||
|
||||
// Terminal height, pixels (e.g., 480)
|
||||
uint32 terminal_height_pixels = 7;
|
||||
|
||||
// Encoded terminal modes
|
||||
bytes encoded_terminal_mode = 8;
|
||||
}
|
||||
|
||||
// X11 forwarding may be requested for a session using the following message
|
||||
message X11ForwardingRequest {
|
||||
// The request type field. This should always be "x11-req"
|
||||
string request_type = 1;
|
||||
|
||||
// The want_reply field on the SSH request
|
||||
bool want_reply = 2;
|
||||
|
||||
// Single connection
|
||||
bool single_connection = 3;
|
||||
|
||||
// X11 authentication protocol
|
||||
string x11_authentication_protocol = 4;
|
||||
|
||||
// X11 authentication cookie
|
||||
string x11_authentication_cookie = 5;
|
||||
|
||||
// X11 screen number
|
||||
uint32 x11_screen_number = 6;
|
||||
}
|
||||
|
||||
// Environment variables may be passed to the shell/command to be
|
||||
// started later.
|
||||
message EnvRequest {
|
||||
// The request type field. This should always be "env"
|
||||
string request_type = 1;
|
||||
|
||||
// The want_reply field on the SSH request
|
||||
bool want_reply = 2;
|
||||
|
||||
// Variable name
|
||||
string variable_name = 3;
|
||||
|
||||
// Variable value
|
||||
string variable_value = 4;
|
||||
}
|
||||
|
||||
// This message will request that the user's default shell (typically
|
||||
// defined in /etc/passwd in UNIX systems) be started at the other end.
|
||||
message ShellRequest {
|
||||
// The request type field. This should always be "shell"
|
||||
string request_type = 1;
|
||||
|
||||
// The want_reply field on the SSH request
|
||||
bool want_reply = 2;
|
||||
}
|
||||
|
||||
// This message will request that the server start the execution of the
|
||||
// given command. The 'command' string may contain a path.
|
||||
message ExecRequest {
|
||||
// The request type field. This should always be "exec"
|
||||
string request_type = 1;
|
||||
|
||||
// The want_reply field on the SSH request
|
||||
bool want_reply = 2;
|
||||
|
||||
// The command to be executed
|
||||
string command = 3;
|
||||
}
|
||||
|
||||
// Execute a predefined subsystem. It is expected that
|
||||
// these will include a general file transfer mechanism, and possibly
|
||||
// other features.
|
||||
message SubsystemRequest {
|
||||
// The request type field. This should always be "subsystem"
|
||||
string request_type = 1;
|
||||
|
||||
// The want_reply field on the SSH request
|
||||
bool want_reply = 2;
|
||||
|
||||
// The predefined subsystem to execute
|
||||
string subsystem_name = 3;
|
||||
}
|
||||
|
||||
// When the window (terminal) size changes on the client side, it MAY
|
||||
// send a message to the other side to inform it of the new dimensions.
|
||||
message WindowChangeRequest {
|
||||
// The request type field. This should always be "window-change"
|
||||
string request_type = 1;
|
||||
|
||||
// The want_reply field on the SSH request. This should always be false.
|
||||
bool want_reply = 2;
|
||||
|
||||
// Terminal width, columns
|
||||
uint32 terminal_width_columns = 3;
|
||||
|
||||
// Terminal height, rows
|
||||
uint32 terminal_height_rows = 4;
|
||||
|
||||
// Terminal width, pixels
|
||||
uint32 terminal_width_pixels = 5;
|
||||
|
||||
// Terminal height, pixels
|
||||
uint32 terminal_height_pixels = 6;
|
||||
}
|
||||
|
||||
// The message below is used by the server to inform the client when it
|
||||
// can or cannot perform flow control (control-S/control-Q processing).
|
||||
// If 'client can do' is TRUE, the client is allowed to do flow control
|
||||
// using control-S and control-Q
|
||||
message XonXoffRequest {
|
||||
// The request type field. This should always be "xon-xoff"
|
||||
string request_type = 1;
|
||||
|
||||
// The want_reply field on the SSH request. This should always be false.
|
||||
bool want_reply = 2;
|
||||
|
||||
// Client can do flow control
|
||||
bool client_can_do = 3;
|
||||
}
|
||||
|
||||
// A signal can be delivered to the remote process/service using the
|
||||
// following message.
|
||||
message SignalRequest {
|
||||
// The request type field. This should always be "signal"
|
||||
string request_type = 1;
|
||||
|
||||
// The want_reply field on the SSH request. This should always be false
|
||||
bool want_reply = 2;
|
||||
|
||||
// Signal name, without the SIG prefix. The 'signal name' is one of the following (these are from [POSIX]).
|
||||
// ABRT ALRM FPE HUP ILL INT KILL
|
||||
// PIPE QUIT SEGV TERM USR1 USR2
|
||||
string signal_name = 3;
|
||||
}
|
||||
|
||||
// When the command running at the other end terminates, the following
|
||||
// message can be sent to return the exit status of the command.
|
||||
message ExitStatusRequest {
|
||||
// The request type field. This should always be "exit-status"
|
||||
string request_type = 1;
|
||||
|
||||
// The want_reply field on the SSH request. This should always be false.
|
||||
bool want_reply = 2;
|
||||
|
||||
// Exit status of the command
|
||||
uint32 exit_status = 3;
|
||||
}
|
||||
|
||||
// The remote command may terminate violently due to a signal.
|
||||
// Such a condition can be indicated by the following message. A zero
|
||||
// 'exit_status' usually means that the command terminated successfully.
|
||||
message ExitSignalRequest {
|
||||
// The request type field. This should always be "exit-signal"
|
||||
string request_type = 1;
|
||||
|
||||
// The want_reply field on the SSH request. This should always be false.
|
||||
bool want_reply = 2;
|
||||
|
||||
// Signal name, without the SIG prefix. The 'signal name' is one of the following (these are from [POSIX]).
|
||||
// ABRT ALRM FPE HUP ILL INT KILL
|
||||
// PIPE QUIT SEGV TERM USR1 USR2
|
||||
string signal_name = 3;
|
||||
|
||||
// Core dumped
|
||||
bool core_dumped = 4;
|
||||
|
||||
// Error message in ISO-10646 UTF-8 encoding
|
||||
string error_message = 5;
|
||||
|
||||
// Language tag from https://www.rfc-editor.org/rfc/rfc3066
|
||||
string language_tag = 6;
|
||||
}
|
||||
|
||||
// The following channel-specific request can be sent over a session
|
||||
// channel to request that the remote host perform
|
||||
// a BREAK operation.
|
||||
message BreakRequest {
|
||||
// The request type field. This should always be "break"
|
||||
string request_type = 1;
|
||||
|
||||
// The want_reply field on the SSH request. This should always be false.
|
||||
bool want_reply = 2;
|
||||
|
||||
// Break-length in milliseconds
|
||||
uint32 break_length_ms = 3;
|
||||
}
|
||||
|
||||
// Fields for the following SSH request protos were taken from
|
||||
// https://www.iana.org/assignments/ssh-parameters/ssh-parameters.xhtml#ssh-parameters-12
|
||||
|
||||
// A party need not explicitly request forwardings from its own end to
|
||||
// the other direction. However, if it wishes that connections to a
|
||||
// port on the other side be forwarded to the local side, it must
|
||||
// explicitly request this.
|
||||
message TCPIPForwardRequest {
|
||||
// The request type field. This should always be "tcpip-forward"
|
||||
string request_type = 1;
|
||||
|
||||
// The want_reply field on the SSH request. This should always be false.
|
||||
bool want_reply = 2;
|
||||
|
||||
// Address to bind (e.g., "0.0.0.0") on which connections for forwarding
|
||||
// are to be accepted
|
||||
string address_to_bind = 3;
|
||||
|
||||
// Port number to bind on which connections for forwarding
|
||||
// are to be accepted
|
||||
uint32 port_to_bind = 4;
|
||||
}
|
||||
|
||||
// A port forwarding can be canceled with the following message. Note
|
||||
// that channel open requests may be received until a reply to this
|
||||
// message is received.
|
||||
message CancelTCPIPForwardRequest {
|
||||
// The request type field. This should always be "cancel-tcpip-forward"
|
||||
string request_type = 1;
|
||||
|
||||
// The want_reply field on the SSH request. This should always be false.
|
||||
bool want_reply = 2;
|
||||
|
||||
// Address to bind (e.g., "0.0.0.0") on which connections for forwarding
|
||||
// are to be accepted
|
||||
string address_to_bind = 3;
|
||||
|
||||
// Port number to bind on which connections for forwarding
|
||||
// are to be accepted
|
||||
uint32 port_to_bind = 4;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
)
|
||||
|
||||
const (
|
||||
Protocol bsr.Protocol = "SSH\x87"
|
||||
|
||||
MaxPacketSize = 256 * 1024
|
||||
|
||||
// Chunk types
|
||||
DataChunkType bsr.ChunkType = "DATA"
|
||||
BreakReqChunkType bsr.ChunkType = "BREK"
|
||||
CancelTCPIPForwardReqChunkType bsr.ChunkType = "CTPF"
|
||||
DirectTCPIPReqChunkType bsr.ChunkType = "DTCP"
|
||||
EnvReqChunkType bsr.ChunkType = "ENVR"
|
||||
ExecReqChunkType bsr.ChunkType = "EXEC"
|
||||
ExitSignalReqChunkType bsr.ChunkType = "EXSG"
|
||||
ExitStatusReqChunkType bsr.ChunkType = "EXST"
|
||||
ForwardedTCPIPReqChunkType bsr.ChunkType = "FTCP"
|
||||
PtyReqChunkType bsr.ChunkType = "PTYR"
|
||||
SessionReqChunkType bsr.ChunkType = "SESS"
|
||||
ShellReqChunkType bsr.ChunkType = "SHLL"
|
||||
SignalReqChunkType bsr.ChunkType = "SGNL"
|
||||
SubsystemReqChunkType bsr.ChunkType = "SUBS"
|
||||
TCPIPForwardReqChunkType bsr.ChunkType = "TCPF"
|
||||
UnknownReqChunkType bsr.ChunkType = "UNKR"
|
||||
WindowChangeReqChunkType bsr.ChunkType = "WCHG"
|
||||
X11ForwardingReqChunkType bsr.ChunkType = "X11F"
|
||||
X11ReqChunkType bsr.ChunkType = "X11R"
|
||||
XonXoffReqChunkType bsr.ChunkType = "XOXO"
|
||||
)
|
||||
|
||||
// DataChunk contains the raw byte data from an SSH session
|
||||
type DataChunk struct {
|
||||
bsr.BaseChunk
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// NewDataChunk constructs a DataChunk
|
||||
func NewDataChunk(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, data []byte) (*DataChunk, error) {
|
||||
const op = "ssh.NewDataChunk"
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, DataChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
return &DataChunk{
|
||||
BaseChunk: *baseChunk,
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MarshalData returns the data for a DataChunk
|
||||
func (c *DataChunk) MarshalData(_ context.Context) ([]byte, error) {
|
||||
return c.Data, nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const BreakRequestType = "break"
|
||||
|
||||
// BreakRequest is a chunk to contain data for an SSH Break request
|
||||
type BreakRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.BreakRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes a BreakRequest chunk
|
||||
func (r *BreakRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(BreakRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.BreakRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewBreakRequest creates a BreakRequest chunk
|
||||
func NewBreakRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*BreakRequest, error) {
|
||||
const op = "ssh.NewBreakRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != BreakRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, BreakRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, BreakReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval breakSigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &BreakRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
BreakRequest: &pssh.BreakRequest{
|
||||
RequestType: r.Type,
|
||||
WantReply: r.WantReply,
|
||||
BreakLengthMs: sigval.BreakLengthMs,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type breakSigval struct {
|
||||
BreakLengthMs uint32
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewBreakRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
message := breakSigval{BreakLengthMs: 123}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewBreakRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: BreakRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewBreakRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: BreakRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewBreakRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewBreakRequest: request type must be "break": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: BreakRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewBreakRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.BreakRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.BreakLengthMs, message.BreakLengthMs)
|
||||
require.Equal(t, dataPayload.RequestType, BreakRequestType)
|
||||
require.Equal(t, dataPayload.WantReply, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const CancelTCPIPForwardRequestType = "cancel-tcpip-forward"
|
||||
|
||||
// CancelTCPIPForwardRequest is a chunk to contain data for an SSH Cancel TCIPIP Forward request
|
||||
type CancelTCPIPForwardRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.CancelTCPIPForwardRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes a CancelTCPIPForwardRequest chunk
|
||||
func (r *CancelTCPIPForwardRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(CancelTCPIPForwardRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.CancelTCPIPForwardRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewCancelTCPIPForwardRequest creates a CancelTCPIPForwardRequest chunk
|
||||
func NewCancelTCPIPForwardRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*CancelTCPIPForwardRequest, error) {
|
||||
const op = "ssh.NewCancelTCPIPForwardRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != CancelTCPIPForwardRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, CancelTCPIPForwardRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, CancelTCPIPForwardReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval cancelTCPIPForwardSigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &CancelTCPIPForwardRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
CancelTCPIPForwardRequest: &pssh.CancelTCPIPForwardRequest{
|
||||
RequestType: r.Type,
|
||||
WantReply: r.WantReply,
|
||||
AddressToBind: sigval.AddressToBind,
|
||||
PortToBind: sigval.PortToBind,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type cancelTCPIPForwardSigval struct {
|
||||
AddressToBind string
|
||||
PortToBind uint32
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewCancelTCPIPForwardRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
message := cancelTCPIPForwardSigval{AddressToBind: "127.0.0.1", PortToBind: 123}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewCancelTCPIPForwardRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: CancelTCPIPForwardRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewCancelTCPIPForwardRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: CancelTCPIPForwardRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewCancelTCPIPForwardRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewCancelTCPIPForwardRequest: request type must be "cancel-tcpip-forward": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: CancelTCPIPForwardRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewCancelTCPIPForwardRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.CancelTCPIPForwardRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.AddressToBind, message.AddressToBind)
|
||||
require.Equal(t, dataPayload.PortToBind, message.PortToBind)
|
||||
require.Equal(t, dataPayload.RequestType, CancelTCPIPForwardRequestType)
|
||||
require.Equal(t, dataPayload.WantReply, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const DirectTCPIPRequestType = "direct-tcpip"
|
||||
|
||||
// DirectTCPIPRequest is a chunk to contain data for an SSH Direct TCPIP request
|
||||
type DirectTCPIPRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.DirectTCPIPRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes a DirectTCPIPRequest chunk
|
||||
func (r *DirectTCPIPRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(DirectTCPIPRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.DirectTCPIPRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewDirectTCPIPRequest creates a DirectTCPIPRequest chunk
|
||||
func NewDirectTCPIPRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*DirectTCPIPRequest, error) {
|
||||
const op = "ssh.NewDirectTCPIPRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != DirectTCPIPRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, DirectTCPIPRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, DirectTCPIPReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval directTCPIPSigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &DirectTCPIPRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
DirectTCPIPRequest: &pssh.DirectTCPIPRequest{
|
||||
RequestType: r.Type,
|
||||
SenderChannel: sigval.SenderChannel,
|
||||
InitialWindowSize: sigval.InitialWindowSize,
|
||||
MaximumPacketSize: sigval.MaximumPacketSize,
|
||||
Host: sigval.Host,
|
||||
Port: sigval.Port,
|
||||
OriginatorIpAddress: sigval.OriginatorIpAddress,
|
||||
OriginatorPort: sigval.OriginatorPort,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type directTCPIPSigval struct {
|
||||
SenderChannel uint32
|
||||
InitialWindowSize uint32
|
||||
MaximumPacketSize uint32
|
||||
Host string
|
||||
Port uint32
|
||||
OriginatorIpAddress string
|
||||
OriginatorPort uint32
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewDirectTCPIPRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
message := directTCPIPSigval{
|
||||
SenderChannel: 123,
|
||||
InitialWindowSize: 500,
|
||||
MaximumPacketSize: 456,
|
||||
Host: "hostarooni",
|
||||
Port: 789,
|
||||
OriginatorIpAddress: "original",
|
||||
OriginatorPort: 99,
|
||||
}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewDirectTCPIPRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: DirectTCPIPRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewDirectTCPIPRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: DirectTCPIPRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewDirectTCPIPRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewDirectTCPIPRequest: request type must be "direct-tcpip": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: DirectTCPIPRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewDirectTCPIPRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.DirectTCPIPRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.SenderChannel, message.SenderChannel)
|
||||
require.Equal(t, dataPayload.InitialWindowSize, message.InitialWindowSize)
|
||||
require.Equal(t, dataPayload.MaximumPacketSize, message.MaximumPacketSize)
|
||||
require.Equal(t, dataPayload.Host, message.Host)
|
||||
require.Equal(t, dataPayload.Port, message.Port)
|
||||
require.Equal(t, dataPayload.OriginatorPort, message.OriginatorPort)
|
||||
require.Equal(t, dataPayload.OriginatorIpAddress, message.OriginatorIpAddress)
|
||||
require.Equal(t, dataPayload.RequestType, DirectTCPIPRequestType)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const EnvRequestType = "env"
|
||||
|
||||
// EnvRequest is a chunk to contain data for an SSH Env request
|
||||
type EnvRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.EnvRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes an EnvRequest chunk
|
||||
func (r *EnvRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(EnvRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.EnvRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewEnvRequest creates an EnvRequest chunk
|
||||
func NewEnvRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*EnvRequest, error) {
|
||||
const op = "ssh.NewEnvRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != EnvRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, EnvRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, EnvReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval envSigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &EnvRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
EnvRequest: &pssh.EnvRequest{
|
||||
RequestType: r.Type,
|
||||
WantReply: r.WantReply,
|
||||
VariableName: sigval.Name,
|
||||
VariableValue: sigval.Value,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type envSigval struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewEnvRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
message := envSigval{Name: "my baloney has a first name", Value: "it's h-o-m-e-r"}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewEnvRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: EnvRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewEnvRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: EnvRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewEnvRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewEnvRequest: request type must be "env": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: EnvRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewEnvRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.EnvRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.VariableName, message.Name)
|
||||
require.Equal(t, dataPayload.VariableValue, message.Value)
|
||||
require.Equal(t, dataPayload.RequestType, EnvRequestType)
|
||||
require.Equal(t, dataPayload.WantReply, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const ExecRequestType = "exec"
|
||||
|
||||
// ExecRequest is a chunk to contain data for an SSH Exec request
|
||||
type ExecRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.ExecRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes an ExecRequest chunk
|
||||
func (r *ExecRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(ExecRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.ExecRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewExecRequest creates an ExecRequest chunk
|
||||
func NewExecRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*ExecRequest, error) {
|
||||
const op = "ssh.NewExecRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != ExecRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, ExecRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, ExecReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval execSigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &ExecRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
ExecRequest: &pssh.ExecRequest{
|
||||
RequestType: r.Type,
|
||||
WantReply: r.WantReply,
|
||||
Command: sigval.Command,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type execSigval struct {
|
||||
Command string
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewExecRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
message := execSigval{Command: "recycle!"}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewExecRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: ExecRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewExecRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: ExecRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewExecRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewExecRequest: request type must be "exec": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: ExecRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewExecRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.ExecRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.Command, message.Command)
|
||||
require.Equal(t, dataPayload.RequestType, ExecRequestType)
|
||||
require.Equal(t, dataPayload.WantReply, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const ExitSignalRequestType = "exit-signal"
|
||||
|
||||
// ExitSignalRequest is a chunk to contain data for an SSH Exit Signal request
|
||||
type ExitSignalRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.ExitSignalRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes an ExitSignalRequest chunk
|
||||
func (r *ExitSignalRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(ExitSignalRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.ExitSignalRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewExitSignalRequest creates an ExitSignalRequest chunk
|
||||
func NewExitSignalRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*ExitSignalRequest, error) {
|
||||
const op = "ssh.NewExitSignalRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != ExitSignalRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, ExitSignalRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, ExitSignalReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval exitSignalSigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &ExitSignalRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
ExitSignalRequest: &pssh.ExitSignalRequest{
|
||||
RequestType: r.Type,
|
||||
WantReply: r.WantReply,
|
||||
SignalName: sigval.SignalName,
|
||||
CoreDumped: sigval.CoreDumped,
|
||||
ErrorMessage: sigval.ErrorMessage,
|
||||
LanguageTag: sigval.LanguageTag,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type exitSignalSigval struct {
|
||||
SignalName string
|
||||
CoreDumped bool
|
||||
ErrorMessage string
|
||||
LanguageTag string
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewExitSignalRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
message := exitSignalSigval{
|
||||
SignalName: "HALT",
|
||||
CoreDumped: true,
|
||||
ErrorMessage: "things did not go as expected",
|
||||
LanguageTag: "EN",
|
||||
}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewExitSignalRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: ExitSignalRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewExitSignalRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: ExitSignalRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewExitSignalRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewExitSignalRequest: request type must be "exit-signal": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: ExitSignalRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewExitSignalRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.ExitSignalRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.SignalName, message.SignalName)
|
||||
require.Equal(t, dataPayload.CoreDumped, message.CoreDumped)
|
||||
require.Equal(t, dataPayload.ErrorMessage, message.ErrorMessage)
|
||||
require.Equal(t, dataPayload.LanguageTag, message.LanguageTag)
|
||||
require.Equal(t, dataPayload.RequestType, ExitSignalRequestType)
|
||||
require.Equal(t, dataPayload.WantReply, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const ExitStatusRequestType = "exit-status"
|
||||
|
||||
// ExitStatusRequest is a chunk to contain data for an SSH Exit Status request
|
||||
type ExitStatusRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.ExitStatusRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes an ExitStatusRequest chunk
|
||||
func (r *ExitStatusRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(ExitStatusRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.ExitStatusRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewExitSignalRequest creates an ExitSignalRequest chunk
|
||||
func NewExitStatusRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*ExitStatusRequest, error) {
|
||||
const op = "ssh.NewExitStatusRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != ExitStatusRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, ExitStatusRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, ExitStatusReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval exitStatusSigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &ExitStatusRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
ExitStatusRequest: &pssh.ExitStatusRequest{
|
||||
RequestType: r.Type,
|
||||
WantReply: r.WantReply,
|
||||
ExitStatus: sigval.ExitStatus,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type exitStatusSigval struct {
|
||||
ExitStatus uint32
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewExitStatusRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
message := exitStatusSigval{
|
||||
ExitStatus: 99,
|
||||
}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewExitStatusRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: ExitStatusRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewExitStatusRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: ExitStatusRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewExitStatusRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewExitStatusRequest: request type must be "exit-status": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: ExitStatusRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewExitStatusRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.ExitStatusRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.ExitStatus, message.ExitStatus)
|
||||
require.Equal(t, dataPayload.RequestType, ExitStatusRequestType)
|
||||
require.Equal(t, dataPayload.WantReply, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const ForwardedTCPIPRequestType = "forwarded-tcpip"
|
||||
|
||||
// ForwardedTCPIPRequest is a chunk to contain data for an SSH Forwarded TCPIP request
|
||||
type ForwardedTCPIPRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.ForwardedTCPIPRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes an ForwardedTCPIPRequest chunk
|
||||
func (r *ForwardedTCPIPRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(ForwardedTCPIPRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.ForwardedTCPIPRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewForwardedTCPIPRequest creates an ForwardedTCPIPRequest chunk
|
||||
func NewForwardedTCPIPRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*ForwardedTCPIPRequest, error) {
|
||||
const op = "ssh.NewForwardedTCPIPRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != ForwardedTCPIPRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, ForwardedTCPIPRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, ForwardedTCPIPReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval forwardedTCPIPRequestSigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &ForwardedTCPIPRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
ForwardedTCPIPRequest: &pssh.ForwardedTCPIPRequest{
|
||||
RequestType: r.Type,
|
||||
SenderChannel: sigval.SenderChannel,
|
||||
InitialWindowSize: sigval.InitialWindowSize,
|
||||
MaximumPacketSize: sigval.MaximumPacketSize,
|
||||
Address: sigval.Address,
|
||||
Port: sigval.Port,
|
||||
OriginatorIpAddress: sigval.OriginatorIpAddress,
|
||||
OriginatorPort: sigval.OriginatorPort,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type forwardedTCPIPRequestSigval struct {
|
||||
SenderChannel uint32
|
||||
InitialWindowSize uint32
|
||||
MaximumPacketSize uint32
|
||||
Address string
|
||||
Port uint32
|
||||
OriginatorIpAddress string
|
||||
OriginatorPort uint32
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewForwardedTCPIPRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
message := forwardedTCPIPRequestSigval{
|
||||
SenderChannel: 123,
|
||||
InitialWindowSize: 456,
|
||||
MaximumPacketSize: 789,
|
||||
Address: "addy",
|
||||
Port: 99,
|
||||
OriginatorIpAddress: "original addy",
|
||||
OriginatorPort: 22,
|
||||
}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewForwardedTCPIPRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: ForwardedTCPIPRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewForwardedTCPIPRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: ForwardedTCPIPRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewForwardedTCPIPRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewForwardedTCPIPRequest: request type must be "forwarded-tcpip": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: ForwardedTCPIPRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewForwardedTCPIPRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.ForwardedTCPIPRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.SenderChannel, message.SenderChannel)
|
||||
require.Equal(t, dataPayload.InitialWindowSize, message.InitialWindowSize)
|
||||
require.Equal(t, dataPayload.MaximumPacketSize, message.MaximumPacketSize)
|
||||
require.Equal(t, dataPayload.Address, message.Address)
|
||||
require.Equal(t, dataPayload.Port, message.Port)
|
||||
require.Equal(t, dataPayload.OriginatorPort, message.OriginatorPort)
|
||||
require.Equal(t, dataPayload.OriginatorIpAddress, message.OriginatorIpAddress)
|
||||
require.Equal(t, dataPayload.RequestType, ForwardedTCPIPRequestType)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const PtyRequestType = "pty-req"
|
||||
|
||||
// PtyPRequest is a chunk to contain data for an SSH Pty request
|
||||
type PtyRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.PtyRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes an PtyRequest chunk
|
||||
func (r *PtyRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(PtyRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.PtyRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewPtyRequest creates an PtyRequest chunk
|
||||
func NewPtyRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*PtyRequest, error) {
|
||||
const op = "ssh.NewPtyRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != PtyRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, PtyRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, PtyReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval ptySigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &PtyRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
PtyRequest: &pssh.PtyRequest{
|
||||
RequestType: r.Type,
|
||||
WantReply: r.WantReply,
|
||||
TermEnvVar: sigval.TermEnvVar,
|
||||
TerminalWidthCharacters: sigval.TerminalWidthCharacters,
|
||||
TerminalHeightRows: sigval.TerminalHeightRows,
|
||||
TerminalWidthPixels: sigval.TerminalWidthPixels,
|
||||
TerminalHeightPixels: sigval.TerminalHeightPixels,
|
||||
EncodedTerminalMode: sigval.EncodedTerminalMode,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type ptySigval struct {
|
||||
TermEnvVar string
|
||||
TerminalWidthCharacters uint32
|
||||
TerminalHeightRows uint32
|
||||
TerminalWidthPixels uint32
|
||||
TerminalHeightPixels uint32
|
||||
EncodedTerminalMode []byte
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewPtyRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
|
||||
// emulate how go ssh encodes the termmode
|
||||
termmodes := gssh.TerminalModes{
|
||||
gssh.ECHO: 0,
|
||||
gssh.TTY_OP_ISPEED: 14400,
|
||||
gssh.TTY_OP_OSPEED: 14400,
|
||||
}
|
||||
|
||||
var tm []byte
|
||||
for k, v := range termmodes {
|
||||
kv := struct {
|
||||
Key byte
|
||||
Val uint32
|
||||
}{k, v}
|
||||
|
||||
tm = append(tm, gssh.Marshal(&kv)...)
|
||||
}
|
||||
tm = append(tm, 0)
|
||||
|
||||
message := ptySigval{
|
||||
TermEnvVar: "var",
|
||||
TerminalWidthCharacters: 123,
|
||||
TerminalHeightRows: 456,
|
||||
TerminalWidthPixels: 789,
|
||||
TerminalHeightPixels: 234,
|
||||
EncodedTerminalMode: tm,
|
||||
}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewPtyRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: PtyRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewPtyRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: PtyRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewPtyRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewPtyRequest: request type must be "pty-req": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: PtyRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewPtyRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.PtyRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.TermEnvVar, message.TermEnvVar)
|
||||
require.Equal(t, dataPayload.TerminalWidthCharacters, message.TerminalWidthCharacters)
|
||||
require.Equal(t, dataPayload.TerminalHeightRows, message.TerminalHeightRows)
|
||||
require.Equal(t, dataPayload.TerminalWidthPixels, message.TerminalWidthPixels)
|
||||
require.Equal(t, dataPayload.TerminalHeightPixels, message.TerminalHeightPixels)
|
||||
require.Equal(t, dataPayload.EncodedTerminalMode, message.EncodedTerminalMode)
|
||||
require.Equal(t, dataPayload.RequestType, PtyRequestType)
|
||||
require.Equal(t, dataPayload.WantReply, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const SessionRequestType = "session"
|
||||
|
||||
// SessionRequest is a chunk to contain data for an SSH Session request
|
||||
type SessionRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.SessionRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes an SessionRequest chunk
|
||||
func (r *SessionRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(SessionRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.SessionRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewSessionRequest creates an SessionRequest chunk
|
||||
func NewSessionRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*SessionRequest, error) {
|
||||
const op = "ssh.NewSessionRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != SessionRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, SessionRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, SessionReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval sessionSigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &SessionRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
SessionRequest: &pssh.SessionRequest{
|
||||
RequestType: r.Type,
|
||||
SenderChannel: sigval.SenderChannel,
|
||||
InitialWindowSize: sigval.InitialWindowSize,
|
||||
MaximumPacketSize: sigval.MaximumPacketSize,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type sessionSigval struct {
|
||||
SenderChannel uint32
|
||||
InitialWindowSize uint32
|
||||
MaximumPacketSize uint32
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewSessionRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
message := sessionSigval{
|
||||
SenderChannel: 123,
|
||||
InitialWindowSize: 456,
|
||||
MaximumPacketSize: 789,
|
||||
}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewSessionRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: SessionRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewSessionRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: SessionRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewSessionRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewSessionRequest: request type must be "session": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: SessionRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewSessionRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.SessionRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.SenderChannel, message.SenderChannel)
|
||||
require.Equal(t, dataPayload.InitialWindowSize, message.InitialWindowSize)
|
||||
require.Equal(t, dataPayload.MaximumPacketSize, message.MaximumPacketSize)
|
||||
require.Equal(t, dataPayload.RequestType, SessionRequestType)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const ShellRequestType = "shell"
|
||||
|
||||
// ShellRequest is a chunk to contain data for an SSH Shell request
|
||||
type ShellRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.ShellRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes an ShellRequest chunk
|
||||
func (r *ShellRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(SessionRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.ShellRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewShellRequest creates an ShellRequest chunk
|
||||
func NewShellRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*ShellRequest, error) {
|
||||
const op = "ssh.NewShellRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != ShellRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, ShellRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, ShellReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
reqData := &ShellRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
ShellRequest: &pssh.ShellRequest{
|
||||
RequestType: r.Type,
|
||||
WantReply: r.WantReply,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewShellRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
payload := []byte{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewShellRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: ShellRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewShellRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: ShellRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewShellRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewShellRequest: request type must be "shell": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: ShellRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewShellRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.ShellRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.RequestType, ShellRequestType)
|
||||
require.Equal(t, dataPayload.WantReply, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const SignalRequestType = "signal"
|
||||
|
||||
// SignalRequest is a chunk to contain data for an SSH Signal request
|
||||
type SignalRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.SignalRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes an SignalRequest chunk
|
||||
func (r *SignalRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(SignalRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.SignalRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewSignalRequest creates an SignalRequest chunk
|
||||
func NewSignalRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*SignalRequest, error) {
|
||||
const op = "ssh.NewSignalRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != SignalRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, SignalRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, SignalReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval signalSigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &SignalRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
SignalRequest: &pssh.SignalRequest{
|
||||
RequestType: r.Type,
|
||||
WantReply: r.WantReply,
|
||||
SignalName: sigval.SignalName,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type signalSigval struct {
|
||||
SignalName string
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewSignalRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
message := signalSigval{
|
||||
SignalName: "HALT",
|
||||
}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewSignalRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: SignalRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewSignalRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: SignalRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewSignalRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewSignalRequest: request type must be "signal": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: SignalRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewSignalRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.SignalRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.SignalName, message.SignalName)
|
||||
require.Equal(t, dataPayload.RequestType, SignalRequestType)
|
||||
require.Equal(t, dataPayload.WantReply, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const SubsystemRequestType = "subsystem"
|
||||
|
||||
// SubsystemRequest is a chunk to contain data for an SSH Subsystem request
|
||||
type SubsystemRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.SubsystemRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes an SubsystemRequest chunk
|
||||
func (r *SubsystemRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(SubsystemRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.SubsystemRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewSubsystemRequest creates an SubsystemRequest chunk
|
||||
func NewSubsystemRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*SubsystemRequest, error) {
|
||||
const op = "ssh.NewSubsystemRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != SubsystemRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, SubsystemRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, SubsystemReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval subsystemSigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &SubsystemRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
SubsystemRequest: &pssh.SubsystemRequest{
|
||||
RequestType: r.Type,
|
||||
WantReply: r.WantReply,
|
||||
SubsystemName: sigval.SubsystemName,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type subsystemSigval struct {
|
||||
SubsystemName string
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewSubsystemRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
message := subsystemSigval{
|
||||
SubsystemName: "submarine",
|
||||
}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewSubsystemRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: SubsystemRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewSubsystemRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: SubsystemRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewSubsystemRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewSubsystemRequest: request type must be "subsystem": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: SubsystemRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewSubsystemRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.SubsystemRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.SubsystemName, message.SubsystemName)
|
||||
require.Equal(t, dataPayload.RequestType, SubsystemRequestType)
|
||||
require.Equal(t, dataPayload.WantReply, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const TCPIPForwardRequestType = "tcpip-forward"
|
||||
|
||||
// TCPIPForwardRequest is a chunk to contain data for an SSH TCPIP Forward request
|
||||
type TCPIPForwardRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.TCPIPForwardRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes an TCPIPForwardRequest chunk
|
||||
func (r *TCPIPForwardRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(TCPIPForwardRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.TCPIPForwardRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewTCPIPForwardRequest creates an TCPIPForwardRequest chunk
|
||||
func NewTCPIPForwardRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*TCPIPForwardRequest, error) {
|
||||
const op = "ssh.NewTCPIPForwardRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != TCPIPForwardRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, TCPIPForwardRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, TCPIPForwardReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval tCPIPForwardSigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &TCPIPForwardRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
TCPIPForwardRequest: &pssh.TCPIPForwardRequest{
|
||||
RequestType: r.Type,
|
||||
WantReply: r.WantReply,
|
||||
AddressToBind: sigval.AddressToBind,
|
||||
PortToBind: sigval.PortToBind,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type tCPIPForwardSigval struct {
|
||||
AddressToBind string
|
||||
PortToBind uint32
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewTCPIPForwardRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
message := tCPIPForwardSigval{
|
||||
AddressToBind: "addy",
|
||||
PortToBind: 99,
|
||||
}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewTCPIPForwardRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: TCPIPForwardRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewTCPIPForwardRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: TCPIPForwardRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewTCPIPForwardRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewTCPIPForwardRequest: request type must be "tcpip-forward": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: TCPIPForwardRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewTCPIPForwardRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.TCPIPForwardRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.AddressToBind, message.AddressToBind)
|
||||
require.Equal(t, dataPayload.PortToBind, message.PortToBind)
|
||||
require.Equal(t, dataPayload.RequestType, TCPIPForwardRequestType)
|
||||
require.Equal(t, dataPayload.WantReply, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// UnknownRequest is a chunk to contain data for any unrecognized SSH request
|
||||
type UnknownRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.UnknownRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes an UnknownRequest chunk
|
||||
func (r *UnknownRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(UnknownRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.UnknownRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewUnknownRequest creates an UnknownRequest chunk
|
||||
func NewUnknownRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*UnknownRequest, error) {
|
||||
const op = "ssh.NewUnknownRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, UnknownReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
return &UnknownRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
UnknownRequest: &pssh.UnknownRequest{
|
||||
RequestType: r.Type,
|
||||
WantReply: r.WantReply,
|
||||
Data: r.Payload,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewUnknownRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
payload := []byte("session request data comin' thru")
|
||||
reqType := "you-dont-know-me"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewUnknownRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: reqType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewUnknownRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: reqType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewUnknownRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: reqType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewUnknownRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.UnknownRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.Data, payload)
|
||||
require.Equal(t, dataPayload.RequestType, reqType)
|
||||
require.Equal(t, dataPayload.WantReply, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const WindowChangeRequestType = "window-change"
|
||||
|
||||
// WindowChangeRequest is a chunk to contain data for an SSH Window Change request
|
||||
type WindowChangeRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.WindowChangeRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes an WindowChangeRequest chunk
|
||||
func (r *WindowChangeRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(WindowChangeRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.WindowChangeRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewWindowChangeRequest creates a WindowChangeRequest chunk
|
||||
func NewWindowChangeRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*WindowChangeRequest, error) {
|
||||
const op = "ssh.NewWindowChangeRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != WindowChangeRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, WindowChangeRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, WindowChangeReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval windowChangeSigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &WindowChangeRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
WindowChangeRequest: &pssh.WindowChangeRequest{
|
||||
RequestType: r.Type,
|
||||
WantReply: r.WantReply,
|
||||
TerminalWidthColumns: sigval.TerminalWidthColumns,
|
||||
TerminalHeightRows: sigval.TerminalHeightRows,
|
||||
TerminalWidthPixels: sigval.TerminalWidthPixels,
|
||||
TerminalHeightPixels: sigval.TerminalHeightPixels,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type windowChangeSigval struct {
|
||||
TerminalWidthColumns uint32
|
||||
TerminalHeightRows uint32
|
||||
TerminalWidthPixels uint32
|
||||
TerminalHeightPixels uint32
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewWindowChangeRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
message := windowChangeSigval{
|
||||
TerminalWidthColumns: 12,
|
||||
TerminalHeightRows: 34,
|
||||
TerminalWidthPixels: 56,
|
||||
TerminalHeightPixels: 78,
|
||||
}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewWindowChangeRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: WindowChangeRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewWindowChangeRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: WindowChangeRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewWindowChangeRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewWindowChangeRequest: request type must be "window-change": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: WindowChangeRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewWindowChangeRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.WindowChangeRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.TerminalWidthColumns, message.TerminalWidthColumns)
|
||||
require.Equal(t, dataPayload.TerminalWidthPixels, message.TerminalWidthPixels)
|
||||
require.Equal(t, dataPayload.TerminalHeightPixels, message.TerminalHeightPixels)
|
||||
require.Equal(t, dataPayload.TerminalHeightRows, message.TerminalHeightRows)
|
||||
require.Equal(t, dataPayload.RequestType, WindowChangeRequestType)
|
||||
require.Equal(t, dataPayload.WantReply, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const X11ForwardingRequestType = "x11-req"
|
||||
|
||||
// X11ForwardingRequest is a chunk to contain data for an SSH X11 Forwarding request
|
||||
type X11ForwardingRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.X11ForwardingRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes an X11ForwardingRequest chunk
|
||||
func (r *X11ForwardingRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(X11ForwardingRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.X11ForwardingRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewX11ForwardingRequest creates an X11ForwardingRequest chunk
|
||||
func NewX11ForwardingRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*X11ForwardingRequest, error) {
|
||||
const op = "ssh.NewX11ForwardingRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != X11ForwardingRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, X11ForwardingRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, X11ForwardingReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval x11ForwardingSigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &X11ForwardingRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
X11ForwardingRequest: &pssh.X11ForwardingRequest{
|
||||
RequestType: r.Type,
|
||||
WantReply: r.WantReply,
|
||||
SingleConnection: sigval.SingleConnection,
|
||||
X11AuthenticationProtocol: sigval.X11AuthenticationProtocol,
|
||||
X11AuthenticationCookie: sigval.X11AuthenticationCookie,
|
||||
X11ScreenNumber: sigval.X11ScreenNumber,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type x11ForwardingSigval struct {
|
||||
SingleConnection bool
|
||||
X11AuthenticationProtocol string
|
||||
X11AuthenticationCookie string
|
||||
X11ScreenNumber uint32
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewX11ForwardingRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
message := x11ForwardingSigval{
|
||||
SingleConnection: true,
|
||||
X11AuthenticationProtocol: "auth",
|
||||
X11AuthenticationCookie: "chocolate chip",
|
||||
X11ScreenNumber: 123,
|
||||
}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewX11ForwardingRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: X11ForwardingRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewX11ForwardingRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: X11ForwardingRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewX11ForwardingRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewX11ForwardingRequest: request type must be "x11-req": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: X11ForwardingRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewX11ForwardingRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.X11ForwardingRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.SingleConnection, message.SingleConnection)
|
||||
require.Equal(t, dataPayload.X11AuthenticationProtocol, message.X11AuthenticationProtocol)
|
||||
require.Equal(t, dataPayload.X11AuthenticationCookie, message.X11AuthenticationCookie)
|
||||
require.Equal(t, dataPayload.X11ScreenNumber, message.X11ScreenNumber)
|
||||
require.Equal(t, dataPayload.RequestType, X11ForwardingRequestType)
|
||||
require.Equal(t, dataPayload.WantReply, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const X11RequestType = "x11"
|
||||
|
||||
// X11Request is a chunk to contain data for an SSH X11 request
|
||||
type X11Request struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.X11Request
|
||||
}
|
||||
|
||||
// MarshalData serializes an X11Request chunk
|
||||
func (r *X11Request) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(X11Request).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.X11Request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewX11Request creates an X11Request chunk
|
||||
func NewX11Request(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*X11Request, error) {
|
||||
const op = "ssh.NewX11Request"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != X11RequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, X11RequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, X11ReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval x11Sigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &X11Request{
|
||||
BaseChunk: *baseChunk,
|
||||
X11Request: &pssh.X11Request{
|
||||
RequestType: r.Type,
|
||||
SenderChannel: sigval.SenderChannel,
|
||||
InitialWindowSize: sigval.InitialWindowSize,
|
||||
MaximumPacketSize: sigval.MaximumPacketSize,
|
||||
OriginatorAddress: sigval.OriginatorAddress,
|
||||
OriginatorPort: sigval.OriginatorPort,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type x11Sigval struct {
|
||||
SenderChannel uint32
|
||||
InitialWindowSize uint32
|
||||
MaximumPacketSize uint32
|
||||
OriginatorAddress string
|
||||
OriginatorPort uint32
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewX11Request(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
message := x11Sigval{
|
||||
SenderChannel: 12,
|
||||
InitialWindowSize: 34,
|
||||
MaximumPacketSize: 56,
|
||||
OriginatorPort: 78,
|
||||
OriginatorAddress: "addy",
|
||||
}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewX11Request: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: X11RequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewX11Request: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: X11RequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewX11Request: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewX11Request: request type must be "x11": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: X11RequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewX11Request(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.X11Request{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.SenderChannel, message.SenderChannel)
|
||||
require.Equal(t, dataPayload.InitialWindowSize, message.InitialWindowSize)
|
||||
require.Equal(t, dataPayload.MaximumPacketSize, message.MaximumPacketSize)
|
||||
require.Equal(t, dataPayload.SenderChannel, message.SenderChannel)
|
||||
require.Equal(t, dataPayload.RequestType, X11RequestType)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/hashicorp/boundary/internal/bsr/internal/is"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const XonXoffRequestType = "xon-xoff"
|
||||
|
||||
// XonXoffRequest is a chunk to contain data for an SSH Xon Xoff request
|
||||
type XonXoffRequest struct {
|
||||
bsr.BaseChunk
|
||||
*pssh.XonXoffRequest
|
||||
}
|
||||
|
||||
// MarshalData serializes an XonXoffRequest chunk
|
||||
func (r *XonXoffRequest) MarshalData(ctx context.Context) ([]byte, error) {
|
||||
const op = "ssh.(XonXoffRequest).MarshalData"
|
||||
dataBytes, err := proto.Marshal(r.XonXoffRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to marshal data: %w", op, err)
|
||||
}
|
||||
|
||||
d := make([]byte, 0, len(dataBytes))
|
||||
d = append(d, dataBytes...)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// NewXonXoffRequest creates an XonXoffRequest chunk
|
||||
func NewXonXoffRequest(ctx context.Context, d bsr.Direction, t *bsr.Timestamp, r *gssh.Request) (*XonXoffRequest, error) {
|
||||
const op = "ssh.NewXonXoffRequest"
|
||||
|
||||
if is.Nil(r) {
|
||||
return nil, fmt.Errorf("%s: request cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if r.Type != XonXoffRequestType {
|
||||
return nil, fmt.Errorf("%s: request type must be %q: %w", op, XonXoffRequestType, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if !bsr.ValidDirection(d) {
|
||||
return nil, fmt.Errorf("%s: invalid direction: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
if is.Nil(t) {
|
||||
return nil, fmt.Errorf("%s: timestamp cannot be nil: %w", op, bsr.ErrInvalidParameter)
|
||||
}
|
||||
|
||||
baseChunk, err := bsr.NewBaseChunk(ctx, Protocol, d, t, XonXoffReqChunkType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to create base chunk: %w", op, err)
|
||||
}
|
||||
|
||||
var sigval xonXoffSigval
|
||||
if err := gssh.Unmarshal(r.Payload, &sigval); err != nil {
|
||||
return nil, fmt.Errorf("%s: unable to unmarshal payload: %w", op, err)
|
||||
}
|
||||
reqData := &XonXoffRequest{
|
||||
BaseChunk: *baseChunk,
|
||||
XonXoffRequest: &pssh.XonXoffRequest{
|
||||
RequestType: r.Type,
|
||||
WantReply: r.WantReply,
|
||||
ClientCanDo: sigval.ClientCanDo,
|
||||
},
|
||||
}
|
||||
return reqData, nil
|
||||
}
|
||||
|
||||
type xonXoffSigval struct {
|
||||
ClientCanDo bool
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/bsr"
|
||||
pssh "github.com/hashicorp/boundary/internal/bsr/gen/ssh/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
gssh "golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Test_NewXonXoffRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := bsr.NewTimestamp(time.Now())
|
||||
message := xonXoffSigval{
|
||||
ClientCanDo: true,
|
||||
}
|
||||
payload := gssh.Marshal(message)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
direction bsr.Direction
|
||||
time *bsr.Timestamp
|
||||
request *gssh.Request
|
||||
expErr bool
|
||||
expErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "nil request",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewXonXoffRequest: request cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty direction",
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: XonXoffRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewXonXoffRequest: invalid direction: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "empty time",
|
||||
direction: bsr.Inbound,
|
||||
request: &gssh.Request{
|
||||
Type: XonXoffRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: "ssh.NewXonXoffRequest: timestamp cannot be nil: invalid parameter",
|
||||
},
|
||||
{
|
||||
name: "bad type",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: "muahaha",
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
expErr: true,
|
||||
expErrMsg: `ssh.NewXonXoffRequest: request type must be "xon-xoff": invalid parameter`,
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
direction: bsr.Inbound,
|
||||
time: now,
|
||||
request: &gssh.Request{
|
||||
Type: XonXoffRequestType,
|
||||
WantReply: false,
|
||||
Payload: payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rc, err := NewXonXoffRequest(ctx, tt.direction, tt.time, tt.request)
|
||||
if tt.expErr {
|
||||
require.EqualError(t, err, tt.expErrMsg)
|
||||
require.Nil(t, rc)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rc)
|
||||
|
||||
data, err := rc.MarshalData(ctx)
|
||||
require.NoError(t, err)
|
||||
dataPayload := &pssh.XonXoffRequest{}
|
||||
err = proto.Unmarshal(data, dataPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dataPayload.ClientCanDo, message.ClientCanDo)
|
||||
require.Equal(t, dataPayload.RequestType, XonXoffRequestType)
|
||||
require.Equal(t, dataPayload.WantReply, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
/*
|
||||
Package ssh defines chunk types for recordings of the ssh protocol.
|
||||
*/
|
||||
package ssh
|
||||
Reference in new issue
Block a user