Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/x/ansi v0.11.8
github.com/charmbracelet/x/term v0.2.2
github.com/coder/websocket v1.8.15
github.com/creack/pty v1.1.24
github.com/hashicorp/yamux v0.1.2
Expand All @@ -19,7 +20,6 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
Expand Down
70 changes: 68 additions & 2 deletions internal/system/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import (
"bytes"
"context"
"crypto/ecdh"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net"
Expand All @@ -19,6 +22,17 @@ import (
"github.com/nicodes/ormos/relay"
)

var terminalSessionRandom = rand.Read

const terminalSessionCreateAttempts = 8

type relayHTTPError struct {
statusCode int
message string
}

func (e *relayHTTPError) Error() string { return e.message }

// PortStatus is one configured exposed port for this system, with whether it
// is currently being served (a local process is listening on it).
type PortStatus struct {
Expand Down Expand Up @@ -428,9 +442,9 @@ func (d *system) relayDo(ctx context.Context, method, path string, body any) ([]
Error string `json:"error"`
}
if json.Unmarshal(data, &e) == nil && e.Error != "" {
return nil, fmt.Errorf("%s", e.Error)
return nil, &relayHTTPError{statusCode: resp.StatusCode, message: e.Error}
}
return nil, fmt.Errorf("relay returned %s", resp.Status)
return nil, &relayHTTPError{statusCode: resp.StatusCode, message: fmt.Sprintf("relay returned %s", resp.Status)}
}
return data, nil
}
Expand Down Expand Up @@ -469,6 +483,58 @@ func (d *system) fetchProjects(ctx context.Context) ([]relay.ProjectInfo, error)
return out.Projects, nil
}

// fetchTerminalSessions lists the persisted terminal tabs for this system.
func (d *system) fetchTerminalSessions(ctx context.Context) ([]relay.TerminalSessionInfo, error) {
data, err := d.relayDo(ctx, http.MethodGet, "/system/terminal-sessions", nil)
if err != nil {
return nil, err
}
var out struct {
Sessions []relay.TerminalSessionInfo `json:"sessions"`
}
if err := json.Unmarshal(data, &out); err != nil {
return nil, err
}
return out.Sessions, nil
}

// createTerminalSession persists a new terminal tab before any local PTY is
// attached. A transport failure is deliberately not retried: the relay may
// have committed the record before the connection failed. Only an explicit
// duplicate response proves that generating a fresh id is safe.
func (d *system) createTerminalSession(ctx context.Context, projectID string) (string, error) {
for range terminalSessionCreateAttempts {
sessionID, err := newTerminalSessionID()
if err != nil {
return "", fmt.Errorf("generate terminal session id: %w", err)
}
_, err = d.relayDo(ctx, http.MethodPost, "/system/terminal-sessions", struct {
ProjectID string `json:"project_id"`
SessionID string `json:"session_id"`
}{ProjectID: projectID, SessionID: sessionID})
if err == nil {
return sessionID, nil
}
var httpErr *relayHTTPError
if !errors.As(err, &httpErr) || httpErr.statusCode != http.StatusConflict {
return "", err
}
}
return "", fmt.Errorf("could not allocate a unique terminal session id")
}

func newTerminalSessionID() (string, error) {
var raw [18]byte // 144 random bits; base64url encodes without punctuation or padding.
n, err := terminalSessionRandom(raw[:])
if err != nil {
return "", err
}
if n != len(raw) {
return "", io.ErrUnexpectedEOF
}
return base64.RawURLEncoding.EncodeToString(raw[:]), nil
}

// createProject creates a project and returns nothing but an error.
func (d *system) createProject(ctx context.Context, name, rootDir string) error {
_, err := d.relayDo(ctx, http.MethodPost, "/system/projects",
Expand Down
Loading