mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
* feat: Add web terminal with reconnecting TTYs This adds a web terminal that can reconnect to resume sessions! No more disconnects, and no more bad bufferring! * Add xstate service * Add the webpage for accessing a web terminal * Add terminal page tests * Use Ticker instead of Timer * Active Windows mode on Windows
78 lines
2.0 KiB
Go
78 lines
2.0 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/peer"
|
|
"github.com/coder/coder/peerbroker/proto"
|
|
)
|
|
|
|
// ReconnectingPTYRequest is sent from the client to the server
|
|
// to pipe data to a PTY.
|
|
type ReconnectingPTYRequest struct {
|
|
Data string `json:"data"`
|
|
Height uint16 `json:"height"`
|
|
Width uint16 `json:"width"`
|
|
}
|
|
|
|
// Conn wraps a peer connection with helper functions to
|
|
// communicate with the agent.
|
|
type Conn struct {
|
|
// Negotiator is responsible for exchanging messages.
|
|
Negotiator proto.DRPCPeerBrokerClient
|
|
|
|
*peer.Conn
|
|
}
|
|
|
|
// ReconnectingPTY returns a connection serving a TTY that can
|
|
// be reconnected to via ID.
|
|
func (c *Conn) ReconnectingPTY(id string, height, width uint16) (net.Conn, error) {
|
|
channel, err := c.Dial(context.Background(), fmt.Sprintf("%s:%d:%d", id, height, width), &peer.ChannelOptions{
|
|
Protocol: "reconnecting-pty",
|
|
})
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("pty: %w", err)
|
|
}
|
|
return channel.NetConn(), nil
|
|
}
|
|
|
|
// SSH dials the built-in SSH server.
|
|
func (c *Conn) SSH() (net.Conn, error) {
|
|
channel, err := c.Dial(context.Background(), "ssh", &peer.ChannelOptions{
|
|
Protocol: "ssh",
|
|
})
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("dial: %w", err)
|
|
}
|
|
return channel.NetConn(), nil
|
|
}
|
|
|
|
// SSHClient calls SSH to create a client that uses a weak cipher
|
|
// for high throughput.
|
|
func (c *Conn) SSHClient() (*ssh.Client, error) {
|
|
netConn, err := c.SSH()
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("ssh: %w", err)
|
|
}
|
|
sshConn, channels, requests, err := ssh.NewClientConn(netConn, "localhost:22", &ssh.ClientConfig{
|
|
// SSH host validation isn't helpful, because obtaining a peer
|
|
// connection already signifies user-intent to dial a workspace.
|
|
// #nosec
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
})
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("ssh conn: %w", err)
|
|
}
|
|
return ssh.NewClient(sshConn, channels, requests), nil
|
|
}
|
|
|
|
func (c *Conn) Close() error {
|
|
_ = c.Negotiator.DRPCConn().Close()
|
|
return c.Conn.Close()
|
|
}
|