Files
teleport/lib/session/session.go
T
Zac Bergquist 25ee7b1343 Convert tsh play to a streaming API (#34547)
* Convert tsh play to use the new streaming API

Instead of downloading the entire session before starting playback,
tsh now streams the recording directly to the terminal. This results
in a significant reduction in "time to first byte" when playing back
large session recordings.

This has the side effect of making playback of SSH consistent with
that of desktop sessions (we no longer "skip" dead time for SSH
sessions and instead play them back at true speed). Additionally,
a new CLI flag was added to support configurable playback speeds.

Fixes #10579
Closes #11385
Updates gravitational/teleport-private#1024

* Start to phase out the GetSessionEvents API

- Mark the old HTTP client implementations as not implemented.
  Nothing is calling these, but they remain for now until we can
  fully remove them from the ClientI interface.
- Move `tsh play --format=json` over to a streaming approach.
  This ensures that we don't load the entire session into memory
  before converting it to JSON/YAML format.

* Clean up terminal escape codes

- Move more utilities into lib/client.Terminal
- Stop timestamping the frame by moving the cursor and writing
  to the terminal - this is racy. Write the timestamp to the
  terminal title instead.

* Update changelog

* Move terminal functions from Unix to common build

This is valid because we initialize terminals on Windows with
ENABLE_VIRTUAL_TERMINAL_PROCESSING, so the same escape sequences
that work on Unix will also work on Windows.

This matches Microsoft's recommendation to use virtual terminal
sequences over the Windows Console API [1].

For more info, see: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences

[1] https://learn.microsoft.com/en-us/windows/console/classic-vs-vt
2024-01-02 19:57:12 +00:00

262 lines
8.2 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Package session is used for bookkeeping of SSH interactive sessions
// that happen in realtime across the teleport cluster
package session
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/moby/term"
"github.com/gravitational/teleport/api/types"
)
// ID is a unique session ID.
type ID string
// IsZero returns true if this ID is empty.
func (s *ID) IsZero() bool {
return len(*s) == 0
}
// String returns string representation of this ID.
func (s *ID) String() string {
return string(*s)
}
// Check will check that the underlying UUID is valid.
func (s *ID) Check() error {
_, err := ParseID(string(*s))
return trace.Wrap(err)
}
// ParseID parses ID and checks if it's correct.
func ParseID(id string) (*ID, error) {
_, err := uuid.Parse(id)
if err != nil {
return nil, trace.BadParameter("%v is not a valid UUID", id)
}
uid := ID(id)
return &uid, nil
}
// NewID returns new session ID. The session ID is based on UUIDv4.
func NewID() ID {
return ID(uuid.New().String())
}
// Session is a session of any kind (SSH, Kubernetes, Desktop, etc)
type Session struct {
// Kind describes what kind of session this is e.g. ssh or k8s.
Kind types.SessionKind `json:"kind"`
// ID is a unique session identifier
ID ID `json:"id"`
// Namespace is a session namespace, separating sessions from each other
Namespace string `json:"namespace"`
// Parties is a list of session parties.
Parties []Party `json:"parties"`
// TerminalParams sets terminal properties
TerminalParams TerminalParams `json:"terminal_params"`
// Login is a login used by all parties joining the session
Login string `json:"login"`
// Created records the information about the time when session
// was created
Created time.Time `json:"created"`
// LastActive holds the information about when the session
// was last active
LastActive time.Time `json:"last_active"`
// ServerID of session
ServerID string `json:"server_id"`
// ServerHostname of session
ServerHostname string `json:"server_hostname"`
// ServerHostPort of session
ServerHostPort int `json:"server_hostport"`
// ServerAddr of session
ServerAddr string `json:"server_addr"`
// ClusterName is the name of the Teleport cluster that this session belongs to.
ClusterName string `json:"cluster_name"`
// KubernetesClusterName is the name of the kube cluster that this session is running in.
KubernetesClusterName string `json:"kubernetes_cluster_name"`
// DesktopName is the name of the desktop that this session is running in.
DesktopName string `json:"desktop_name"`
// DatabaseName is the name of the database being accessed.
DatabaseName string `json:"database_name"`
// AppName is the name of the app being accessed.
AppName string `json:"app_name"`
// Owner is the name of the session owner, ie the one who created the session.
Owner string `json:"owner"`
// Moderated is true if the session requires moderation (only relevant for Kind = ssh/k8s).
Moderated bool `json:"moderated"`
// Command is the command that was executed to start the session.
Command string `json:"command"`
}
// FileTransferRequestParams contain parameters for requesting a file transfer
type FileTransferRequestParams struct {
// Download is true if the request is a download, false if it is an upload
Download bool `json:"direction"`
// Location is location of file to download, or where to put an upload
Location string `json:"location"`
// Filename is the name of the file to be uploaded
Filename string `json:"filename"`
// Requester is the authenticated Teleport user who requested the file transfer
Requester string `json:"requester"`
// Approvers is a list of teleport users who have approved the file transfer request
Approvers []Party `json:"approvers"`
}
// FileTransferDecisionParams contains parameters for approving or denying a file transfer request
type FileTransferDecisionParams struct {
// RequestID is the ID of the request being responded to
RequestID string `json:"requestId"`
// Approved is true if the response approves a file transfer request
Approved bool `json:"approved"`
}
// Participants returns the usernames of the current session participants.
func (s *Session) Participants() []string {
participants := make([]string, 0, len(s.Parties))
for _, p := range s.Parties {
participants = append(participants, p.User)
}
return participants
}
// RemoveParty helper allows to remove a party by it's ID from the
// session's list. Returns 'false' if pid couldn't be found
func (s *Session) RemoveParty(pid ID) bool {
for i := range s.Parties {
if s.Parties[i].ID == pid {
s.Parties = append(s.Parties[:i], s.Parties[i+1:]...)
return true
}
}
return false
}
// Party is a participant a user or a script executing some action
// in the context of the session
type Party struct {
// ID is a unique party id
ID ID `json:"id"`
// Site is a remote address?
RemoteAddr string `json:"remote_addr"`
// User is a teleport user using this session
User string `json:"user"`
// ServerID is an address of the server
ServerID string `json:"server_id"`
// LastActive is a last time this party was active
LastActive time.Time `json:"last_active"`
}
// String returns debug friendly representation
func (p *Party) String() string {
return fmt.Sprintf(
"party(id=%v, remote=%v, user=%v, server=%v, last_active=%v)",
p.ID, p.RemoteAddr, p.User, p.ServerID, p.LastActive,
)
}
// TerminalParams holds the terminal size in a session.
type TerminalParams struct {
W int `json:"w"`
H int `json:"h"`
}
// UnmarshalTerminalParams takes a serialized string that contains the
// terminal parameters and returns a *TerminalParams.
func UnmarshalTerminalParams(s string) (*TerminalParams, error) {
parts := strings.Split(s, ":")
if len(parts) != 2 {
return nil, trace.BadParameter("failed to unmarshal: too many parts")
}
w, err := strconv.Atoi(parts[0])
if err != nil {
return nil, trace.Wrap(err)
}
h, err := strconv.Atoi(parts[1])
if err != nil {
return nil, trace.Wrap(err)
}
return &TerminalParams{
W: w,
H: h,
}, nil
}
// Serialize is a more strict version of String(): it returns a string
// representation of terminal size, this is used in our APIs.
// Format : "W:H"
// Example: "80:25"
func (p *TerminalParams) Serialize() string {
return fmt.Sprintf("%d:%d", p.W, p.H)
}
// String returns debug friendly representation of terminal
func (p *TerminalParams) String() string {
return fmt.Sprintf("TerminalParams(w=%v, h=%v)", p.W, p.H)
}
// Winsize returns low-level parameters for changing PTY
func (p *TerminalParams) Winsize() *term.Winsize {
return &term.Winsize{
Width: uint16(p.W),
Height: uint16(p.H),
}
}
// MaxSessionSliceLength is the maximum number of sessions per time window
// that the backend will return.
const MaxSessionSliceLength = 1000
// NewTerminalParamsFromUint32 returns new terminal parameters from uint32 width and height
func NewTerminalParamsFromUint32(w uint32, h uint32) (*TerminalParams, error) {
if w > maxSize || w < minSize {
return nil, trace.BadParameter("bad width")
}
if h > maxSize || h < minSize {
return nil, trace.BadParameter("bad height")
}
return &TerminalParams{W: int(w), H: int(h)}, nil
}
// NewTerminalParamsFromInt returns new terminal parameters from int width and height
func NewTerminalParamsFromInt(w int, h int) (*TerminalParams, error) {
if w > maxSize || w < minSize {
return nil, trace.BadParameter("bad width")
}
if h > maxSize || h < minSize {
return nil, trace.BadParameter("bad height")
}
return &TerminalParams{W: w, H: h}, nil
}
const (
minSize = 1
maxSize = 4096
)