Skip to content

Commit 72b8264

Browse files
feat: added channel type classification
1 parent a50afd6 commit 72b8264

5 files changed

Lines changed: 93 additions & 69 deletions

File tree

packages/api/model.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,7 @@ type UploadSessionLogEntry struct {
868868
type UploadTerminalEvent struct {
869869
Timestamp time.Time `json:"timestamp"`
870870
EventType string `json:"eventType"`
871+
ChannelType string `json:"channelType,omitempty"`
871872
Data []byte `json:"data"`
872873
ElapsedTime float64 `json:"elapsedTime"`
873874
}

packages/pam/handlers/ssh/proxy.go

Lines changed: 70 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,19 @@ type SSHProxyConfig struct {
2828

2929
// SSHProxy handles proxying SSH connections with credential injection
3030
type SSHProxy struct {
31-
config SSHProxyConfig
31+
config SSHProxyConfig
32+
mutex sync.Mutex
33+
sessionData []byte // Store session data for logging
34+
inputBuffer []byte // Buffer for input data to batch keystrokes
35+
inputChannelType session.TerminalChannelType // Channel type for buffered input
36+
}
37+
38+
// channelState holds per-channel state for tracking session type
39+
type channelState struct {
3240
mutex sync.Mutex
33-
sessionData []byte // Store session data for logging
34-
inputBuffer []byte // Buffer for input data to batch keystrokes
35-
isBinarySession bool // True if this is SFTP/SCP binary protocol (don't log raw data)
36-
sftpParser *SFTPParser // Parser for SFTP protocol to extract file operations
41+
channelType session.TerminalChannelType // Type of channel (terminal, exec, sftp)
42+
isBinarySession bool // True if this channel is SFTP/SCP binary protocol
43+
sftpParser *SFTPParser // Parser for SFTP protocol to extract file operations
3744
}
3845

3946
// NewSSHProxy creates a new SSH proxy instance
@@ -222,22 +229,25 @@ func (p *SSHProxy) handleChannel(ctx context.Context, newChannel ssh.NewChannel,
222229
Str("channelType", channelType).
223230
Msg("SSH channel established")
224231

232+
// Create per-channel state for tracking binary sessions (SFTP/SCP)
233+
chState := &channelState{}
234+
225235
// Handle requests for this channel (pty-req, shell, exec, etc.)
226-
go p.handleChannelRequests(clientRequests, serverChannel, sessionID, channelType)
227-
go p.handleChannelRequests(serverRequests, clientChannel, sessionID, channelType)
236+
go p.handleChannelRequests(clientRequests, serverChannel, sessionID, channelType, chState)
237+
go p.handleChannelRequests(serverRequests, clientChannel, sessionID, channelType, chState)
228238

229239
// Proxy data bidirectionally with logging
230240
errChan := make(chan error, 2)
231241

232242
// Client to Server
233243
go func() {
234-
err := p.proxyData(clientChannel, serverChannel, "client→server", sessionID, true)
244+
err := p.proxyData(clientChannel, serverChannel, "client→server", sessionID, true, chState)
235245
errChan <- err
236246
}()
237247

238248
// Server to Client
239249
go func() {
240-
err := p.proxyData(serverChannel, clientChannel, "server→client", sessionID, false)
250+
err := p.proxyData(serverChannel, clientChannel, "server→client", sessionID, false, chState)
241251
errChan <- err
242252
}()
243253

@@ -258,7 +268,7 @@ func (p *SSHProxy) handleChannel(ctx context.Context, newChannel ssh.NewChannel,
258268
}
259269

260270
// handleChannelRequests handles channel-specific requests (pty, shell, exec, etc.)
261-
func (p *SSHProxy) handleChannelRequests(requests <-chan *ssh.Request, targetChannel ssh.Channel, sessionID string, channelType string) {
271+
func (p *SSHProxy) handleChannelRequests(requests <-chan *ssh.Request, targetChannel ssh.Channel, sessionID string, channelType string, chState *channelState) {
262272
for req := range requests {
263273
log.Debug().
264274
Str("sessionID", sessionID).
@@ -278,15 +288,17 @@ func (p *SSHProxy) handleChannelRequests(requests <-chan *ssh.Request, targetCha
278288

279289
// Determine the type of operation for better logging
280290
opType := "exec"
291+
chState.mutex.Lock()
292+
chState.channelType = session.TerminalChannelExec
281293
if strings.HasPrefix(command, "scp ") {
282294
opType = "scp"
283-
// Mark as binary session so we don't log the raw file data
284-
p.mutex.Lock()
285-
p.isBinarySession = true
286-
p.mutex.Unlock()
295+
// Mark this channel as binary so we don't log the raw file data
296+
chState.isBinarySession = true
297+
chState.channelType = session.TerminalChannelSFTP // SCP is file transfer
287298
} else if strings.Contains(command, "sftp") {
288299
opType = "sftp"
289300
}
301+
chState.mutex.Unlock()
290302

291303
log.Info().
292304
Str("sessionID", sessionID).
@@ -296,27 +308,31 @@ func (p *SSHProxy) handleChannelRequests(requests <-chan *ssh.Request, targetCha
296308

297309
// Log the exec command to the session recording
298310
var logMessage string
311+
var channelType session.TerminalChannelType
299312
if opType == "scp" {
313+
channelType = session.TerminalChannelSFTP
300314
// Parse SCP command for more readable logging
301315
// scp -t /path = receiving file TO server
302316
// scp -f /path = sending file FROM server
303317
if strings.Contains(command, " -t ") {
304318
path := extractSCPPath(command)
305-
logMessage = fmt.Sprintf("[SCP UPLOAD] Uploading file to: %s\n", path)
319+
logMessage = fmt.Sprintf("Uploaded file: %s\n", path)
306320
} else if strings.Contains(command, " -f ") {
307321
path := extractSCPPath(command)
308-
logMessage = fmt.Sprintf("[SCP DOWNLOAD] Downloading file from: %s\n", path)
322+
logMessage = fmt.Sprintf("Downloaded file: %s\n", path)
309323
} else {
310324
logMessage = fmt.Sprintf("$ %s\n", command)
311325
}
312326
} else {
327+
channelType = session.TerminalChannelExec
313328
logMessage = fmt.Sprintf("$ %s\n", command)
314329
}
315330

316331
event := session.TerminalEvent{
317-
Timestamp: time.Now(),
318-
EventType: session.TerminalEventInput,
319-
Data: []byte(logMessage),
332+
Timestamp: time.Now(),
333+
EventType: session.TerminalEventInput,
334+
ChannelType: channelType,
335+
Data: []byte(logMessage),
320336
}
321337
if err := p.config.SessionLogger.LogTerminalEvent(event); err != nil {
322338
log.Error().Err(err).
@@ -327,6 +343,9 @@ func (p *SSHProxy) handleChannelRequests(requests <-chan *ssh.Request, targetCha
327343
}
328344
}
329345
case "shell":
346+
chState.mutex.Lock()
347+
chState.channelType = session.TerminalChannelShell
348+
chState.mutex.Unlock()
330349
log.Info().
331350
Str("sessionID", sessionID).
332351
Msg("SSH interactive shell requested")
@@ -344,15 +363,17 @@ func (p *SSHProxy) handleChannelRequests(requests <-chan *ssh.Request, targetCha
344363

345364
// Log SFTP sessions and set up SFTP parser for file operation logging
346365
if subsystem == "sftp" {
347-
p.mutex.Lock()
348-
p.isBinarySession = true
349-
p.sftpParser = NewSFTPParser()
350-
p.mutex.Unlock()
366+
chState.mutex.Lock()
367+
chState.channelType = session.TerminalChannelSFTP
368+
chState.isBinarySession = true
369+
chState.sftpParser = NewSFTPParser()
370+
chState.mutex.Unlock()
351371

352372
event := session.TerminalEvent{
353-
Timestamp: time.Now(),
354-
EventType: session.TerminalEventInput,
355-
Data: []byte("File transfer session started\n"),
373+
Timestamp: time.Now(),
374+
EventType: session.TerminalEventInput,
375+
ChannelType: session.TerminalChannelSFTP,
376+
Data: []byte("File transfer session started\n"),
356377
}
357378
if err := p.config.SessionLogger.LogTerminalEvent(event); err != nil {
358379
log.Error().Err(err).
@@ -401,7 +422,7 @@ func (p *SSHProxy) handleChannelRequests(requests <-chan *ssh.Request, targetCha
401422
}
402423

403424
// proxyData proxies data between channels with optional logging
404-
func (p *SSHProxy) proxyData(src io.Reader, dst io.Writer, direction string, sessionID string, logInput bool) error {
425+
func (p *SSHProxy) proxyData(src io.Reader, dst io.Writer, direction string, sessionID string, logInput bool, chState *channelState) error {
405426
buf := make([]byte, 32*1024) // 32KB buffer
406427

407428
// Flush any remaining input buffer on exit
@@ -414,11 +435,12 @@ func (p *SSHProxy) proxyData(src io.Reader, dst io.Writer, direction string, ses
414435
for {
415436
n, err := src.Read(buf)
416437
if n > 0 {
417-
// Check if this is a binary session (SFTP/SCP)
418-
p.mutex.Lock()
419-
isBinary := p.isBinarySession
420-
sftpParser := p.sftpParser
421-
p.mutex.Unlock()
438+
// Check if this channel is a binary session (SFTP/SCP)
439+
chState.mutex.Lock()
440+
isBinary := chState.isBinarySession
441+
sftpParser := chState.sftpParser
442+
channelType := chState.channelType
443+
chState.mutex.Unlock()
422444

423445
if isBinary && sftpParser != nil && logInput {
424446
// Parse SFTP packets from client->server direction to extract file operations
@@ -427,9 +449,10 @@ func (p *SSHProxy) proxyData(src io.Reader, dst io.Writer, direction string, ses
427449
// Log each SFTP operation
428450
logMsg := FormatOperation(op) + "\n"
429451
event := session.TerminalEvent{
430-
Timestamp: time.Now(),
431-
EventType: session.TerminalEventInput,
432-
Data: []byte(logMsg),
452+
Timestamp: time.Now(),
453+
EventType: session.TerminalEventInput,
454+
ChannelType: session.TerminalChannelSFTP,
455+
Data: []byte(logMsg),
433456
}
434457
if err := p.config.SessionLogger.LogTerminalEvent(event); err != nil {
435458
log.Error().Err(err).
@@ -448,13 +471,14 @@ func (p *SSHProxy) proxyData(src io.Reader, dst io.Writer, direction string, ses
448471
} else if !isBinary {
449472
// Regular terminal session logging
450473
if logInput {
451-
p.bufferInput(buf[:n], sessionID)
474+
p.bufferInput(buf[:n], sessionID, channelType)
452475
} else {
453476
// For output, log immediately as before
454477
event := session.TerminalEvent{
455-
Timestamp: time.Now(),
456-
EventType: session.TerminalEventOutput,
457-
Data: make([]byte, n),
478+
Timestamp: time.Now(),
479+
EventType: session.TerminalEventOutput,
480+
ChannelType: channelType,
481+
Data: make([]byte, n),
458482
}
459483
copy(event.Data, buf[:n])
460484

@@ -487,10 +511,12 @@ func (p *SSHProxy) proxyData(src io.Reader, dst io.Writer, direction string, ses
487511
}
488512

489513
// bufferInput accumulates input data and logs only when newline or control chars are encountered
490-
func (p *SSHProxy) bufferInput(data []byte, sessionID string) {
514+
func (p *SSHProxy) bufferInput(data []byte, sessionID string, channelType session.TerminalChannelType) {
491515
p.mutex.Lock()
492516
defer p.mutex.Unlock()
493517

518+
p.inputChannelType = channelType
519+
494520
for _, b := range data {
495521
p.inputBuffer = append(p.inputBuffer, b)
496522

@@ -516,9 +542,10 @@ func (p *SSHProxy) flushInputBufferUnsafe(sessionID string) {
516542
}
517543

518544
event := session.TerminalEvent{
519-
Timestamp: time.Now(),
520-
EventType: session.TerminalEventInput,
521-
Data: make([]byte, len(p.inputBuffer)),
545+
Timestamp: time.Now(),
546+
EventType: session.TerminalEventInput,
547+
ChannelType: p.inputChannelType,
548+
Data: make([]byte, len(p.inputBuffer)),
522549
}
523550
copy(event.Data, p.inputBuffer)
524551

packages/pam/local/ssh-proxy.go

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os/exec"
1010
"os/signal"
1111
"strconv"
12+
"strings"
1213
"syscall"
1314
"time"
1415

@@ -207,31 +208,15 @@ func (p *SSHProxyServer) launchSSHClient(username string) error {
207208

208209
// formatSSHArgs formats SSH arguments for logging, quoting args with spaces
209210
func formatSSHArgs(args []string) string {
210-
var formatted []string
211-
for _, arg := range args {
212-
if containsSpace(arg) {
213-
formatted = append(formatted, fmt.Sprintf("%q", arg))
211+
formatted := make([]string, len(args))
212+
for i, arg := range args {
213+
if strings.ContainsRune(arg, ' ') {
214+
formatted[i] = fmt.Sprintf("%q", arg)
214215
} else {
215-
formatted = append(formatted, arg)
216+
formatted[i] = arg
216217
}
217218
}
218-
result := ""
219-
for i, f := range formatted {
220-
if i > 0 {
221-
result += " "
222-
}
223-
result += f
224-
}
225-
return result
226-
}
227-
228-
func containsSpace(s string) bool {
229-
for _, c := range s {
230-
if c == ' ' {
231-
return true
232-
}
233-
}
234-
return false
219+
return strings.Join(formatted, " ")
235220
}
236221

237222
func (p *SSHProxyServer) waitForSSHCompletion() {

packages/pam/session/logger.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,22 @@ const (
3232
TerminalEventOutput TerminalEventType = "output" // Data from server to user
3333
)
3434

35+
// TerminalChannelType represents the type of SSH channel
36+
type TerminalChannelType string
37+
38+
const (
39+
TerminalChannelShell TerminalChannelType = "terminal" // Interactive shell session
40+
TerminalChannelExec TerminalChannelType = "exec" // Single command execution
41+
TerminalChannelSFTP TerminalChannelType = "sftp" // SFTP file transfer
42+
)
43+
3544
// TerminalEvent represents a single event in a terminal session
3645
type TerminalEvent struct {
37-
Timestamp time.Time `json:"timestamp"`
38-
EventType TerminalEventType `json:"eventType"`
39-
Data []byte `json:"data"` // Raw terminal data
40-
ElapsedTime float64 `json:"elapsedTime"` // Seconds since session start (for replay)
46+
Timestamp time.Time `json:"timestamp"`
47+
EventType TerminalEventType `json:"eventType"`
48+
ChannelType TerminalChannelType `json:"channelType,omitempty"` // Type of SSH channel
49+
Data []byte `json:"data"` // Raw terminal data
50+
ElapsedTime float64 `json:"elapsedTime"` // Seconds since session start (for replay)
4151
}
4252

4353
type HttpEventType string

packages/pam/session/uploader.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ func (su *SessionUploader) uploadSessionFile(fileInfo *SessionFileInfo) error {
296296
logs = append(logs, api.UploadTerminalEvent{
297297
Timestamp: event.Timestamp,
298298
EventType: string(event.EventType),
299+
ChannelType: string(event.ChannelType),
299300
Data: event.Data,
300301
ElapsedTime: event.ElapsedTime,
301302
})

0 commit comments

Comments
 (0)