You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
151 lines
5.4 KiB
151 lines
5.4 KiB
package directstream
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
func sharedTestSocket(t *testing.T) string {
|
|
t.Helper()
|
|
dir, err := os.MkdirTemp("", "shared-")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = os.RemoveAll(dir) })
|
|
return filepath.Join(dir, "s.sock")
|
|
}
|
|
|
|
func TestBrowserAndTerminalSessionsShareOneUARTAndCombinedLimit(t *testing.T) {
|
|
factory := newFakeTransportFactory()
|
|
broker, _ := NewBroker(factory, BrokerOptions{})
|
|
defer broker.Close()
|
|
path := sharedTestSocket(t)
|
|
listener, err := ListenTicketSocket(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Close()
|
|
go ServeSharedSocket(listener, broker)
|
|
client := SharedSocketClient{Path: path}
|
|
authority := &websocketAuthority{store: NewTicketStore(TicketStoreOptions{})}
|
|
server := httptest.NewServer(NewWebSocketHandler(broker, authority, authority, WebSocketOptions{}))
|
|
defer server.Close()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
var browsers []*websocket.Conn
|
|
for _, id := range []string{"browser-1", "browser-2"} {
|
|
claims := TicketClaims{SessionID: id, Channel: "CH20", Actor: id, Access: AccessWriter, MaxAttachments: 4}
|
|
connection := dialDirectWebSocket(t, server.URL, claims, server.URL, true)
|
|
defer connection.Close()
|
|
authenticateWebSocket(t, connection, issueWebSocketTicket(t, authority.store, claims))
|
|
status := readStatusFrame(t, connection)
|
|
if !status.Writer || status.QueuePosition != 0 {
|
|
t.Fatalf("browser waiting: %+v", status)
|
|
}
|
|
_ = readWelcomeFrame(t, connection)
|
|
browsers = append(browsers, connection)
|
|
}
|
|
var terminals []SharedAttachment
|
|
for _, id := range []string{"ssh-operation-1", "telnet-operation-2"} {
|
|
attachment, err := client.Attach(ctx, AttachRequest{SessionID: id, Channel: "CH20", Actor: id, Access: AccessWriter, MaxAttachments: 4})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer attachment.Close()
|
|
terminals = append(terminals, attachment)
|
|
}
|
|
if _, err := client.Attach(ctx, AttachRequest{SessionID: "fifth", Channel: "CH20", Actor: "fifth", Access: AccessWriter, MaxAttachments: 4}); !errors.Is(err, ErrAttachmentLimit) {
|
|
t.Fatalf("fifth terminal: %v", err)
|
|
}
|
|
claims := TicketClaims{SessionID: "fifth-browser", Channel: "CH20", Actor: "fifth", Access: AccessWriter, MaxAttachments: 4}
|
|
fifth := dialDirectWebSocket(t, server.URL, claims, server.URL, true)
|
|
defer fifth.Close()
|
|
authenticateWebSocket(t, fifth, issueWebSocketTicket(t, authority.store, claims))
|
|
_ = fifth.SetReadDeadline(time.Now().Add(time.Second))
|
|
if _, _, err := fifth.ReadMessage(); err == nil {
|
|
t.Fatal("fifth browser accepted")
|
|
}
|
|
transport := factory.transport("CH20")
|
|
for _, browser := range browsers {
|
|
if err := browser.WriteMessage(websocket.BinaryMessage, []byte("web\r")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for _, terminal := range terminals {
|
|
if err := terminal.Write(ctx, []byte("terminal\r")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if !waitFor(time.Second, func() bool { return len(transport.written()) == 26 }) {
|
|
t.Fatalf("UART writes: %q", transport.written())
|
|
}
|
|
payload := []byte{0, 0xff, 'o', 'k', '\r', '\n'}
|
|
transport.emit(payload)
|
|
for _, browser := range browsers {
|
|
_ = browser.SetReadDeadline(time.Now().Add(time.Second))
|
|
kind, actual, err := browser.ReadMessage()
|
|
if err != nil || kind != websocket.BinaryMessage || !bytes.Equal(actual, payload) {
|
|
t.Fatalf("browser RX: %v %v", actual, err)
|
|
}
|
|
}
|
|
for _, terminal := range terminals {
|
|
actual, err := terminal.Receive(ctx)
|
|
if err != nil || !bytes.Equal(actual, payload) {
|
|
t.Fatalf("terminal RX: %v %v", actual, err)
|
|
}
|
|
}
|
|
if err := terminals[0].SendBreak(ctx); err != nil || transport.breakCount() != 1 {
|
|
t.Fatalf("break: %v", err)
|
|
}
|
|
_ = terminals[0].Close()
|
|
if !waitFor(time.Second, func() bool { return broker.Snapshot().TotalAttachments == 3 }) {
|
|
t.Fatal("closed terminal slot leaked")
|
|
}
|
|
replacement, err := client.Attach(ctx, AttachRequest{SessionID: "replacement", Channel: "CH20", Actor: "replacement", Access: AccessWriter, MaxAttachments: 4})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer replacement.Close()
|
|
if err := terminals[1].Write(ctx, []byte("still-connected\r")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if snapshot := broker.Snapshot(); snapshot.TransportOpens != 1 || snapshot.TotalAttachments != 4 {
|
|
t.Fatalf("snapshot: %+v", snapshot)
|
|
}
|
|
}
|
|
|
|
func TestSharedSocketHonorsLowerConfiguredLimitAndViewerAccess(t *testing.T) {
|
|
factory := newFakeTransportFactory()
|
|
broker, _ := NewBroker(factory, BrokerOptions{})
|
|
defer broker.Close()
|
|
path := sharedTestSocket(t)
|
|
listener, err := ListenTicketSocket(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Close()
|
|
go ServeSharedSocket(listener, broker)
|
|
client := SharedSocketClient{Path: path}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
viewer, err := client.Attach(ctx, AttachRequest{SessionID: "viewer", Channel: "CH01", Actor: "viewer", Access: AccessViewer, SharedWrite: true, MaxAttachments: 1})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer viewer.Close()
|
|
if err := viewer.Write(ctx, []byte("denied")); err == nil {
|
|
t.Fatal("viewer wrote data")
|
|
}
|
|
if _, err := client.Attach(ctx, AttachRequest{SessionID: "writer", Channel: "CH01", Actor: "writer", Access: AccessWriter, MaxAttachments: 4}); !errors.Is(err, ErrAttachmentLimit) {
|
|
t.Fatalf("configured limit bypassed: %v", err)
|
|
}
|
|
}
|
|
|