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.
226 lines
6.0 KiB
226 lines
6.0 KiB
package directstream
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
const DefaultSharedSocketPath = "/run/tera-api/shared-stream.sock"
|
|
|
|
// SharedAttachment is one authenticated terminal, not an aggregate per service.
|
|
type SharedAttachment interface {
|
|
Receive(context.Context) ([]byte, error)
|
|
Write(context.Context, []byte) error
|
|
SendBreak(context.Context) error
|
|
Close() error
|
|
}
|
|
|
|
type sharedFrame struct {
|
|
Type string `json:"type"`
|
|
Data []byte `json:"data,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// ServeSharedSocket must only be exposed on a service-group-owned Unix socket.
|
|
// The gateway has authenticated the user before it sends an attachment request.
|
|
func ServeSharedSocket(listener net.Listener, broker *Broker) error {
|
|
server := &http.Server{ReadHeaderTimeout: 5 * time.Second, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
upgrader := websocket.Upgrader{}
|
|
connection, err := upgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer connection.Close()
|
|
connection.SetReadLimit(96 * 1024)
|
|
_ = connection.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
var request AttachRequest
|
|
if connection.ReadJSON(&request) != nil {
|
|
return
|
|
}
|
|
request.SharedWrite = request.Access == AccessWriter
|
|
attachment, err := broker.Attach(r.Context(), request)
|
|
if err != nil {
|
|
_ = connection.WriteJSON(sharedFrame{Type: "attached", Error: err.Error()})
|
|
return
|
|
}
|
|
defer attachment.Close()
|
|
_ = connection.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
|
if connection.WriteJSON(sharedFrame{Type: "attached"}) != nil {
|
|
return
|
|
}
|
|
_ = connection.SetReadDeadline(time.Time{})
|
|
ctx, cancel := context.WithCancel(r.Context())
|
|
defer cancel()
|
|
var writeMu sync.Mutex
|
|
write := func(frame sharedFrame) error {
|
|
writeMu.Lock()
|
|
defer writeMu.Unlock()
|
|
_ = connection.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
|
return connection.WriteJSON(frame)
|
|
}
|
|
go func() {
|
|
defer connection.Close()
|
|
for {
|
|
payload, err := attachment.Receive(ctx)
|
|
if err != nil || write(sharedFrame{Type: "data", Data: payload}) != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
for {
|
|
var frame sharedFrame
|
|
if connection.ReadJSON(&frame) != nil {
|
|
return
|
|
}
|
|
switch frame.Type {
|
|
case "write":
|
|
err = attachment.Write(ctx, frame.Data)
|
|
case "break":
|
|
err = attachment.SendBreak(ctx)
|
|
default:
|
|
return
|
|
}
|
|
ack := sharedFrame{Type: "ack"}
|
|
if err != nil {
|
|
ack.Error = err.Error()
|
|
}
|
|
if write(ack) != nil {
|
|
return
|
|
}
|
|
}
|
|
})}
|
|
return server.Serve(listener)
|
|
}
|
|
|
|
type SharedSocketClient struct{ Path string }
|
|
|
|
func (c SharedSocketClient) Attach(ctx context.Context, request AttachRequest) (SharedAttachment, error) {
|
|
path := c.Path
|
|
if path == "" {
|
|
path = DefaultSharedSocketPath
|
|
}
|
|
dialer := websocket.Dialer{HandshakeTimeout: 10 * time.Second, NetDialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
return (&net.Dialer{Timeout: 10 * time.Second}).DialContext(ctx, "unix", path)
|
|
}}
|
|
connection, _, err := dialer.DialContext(ctx, "ws://localhost/stream", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stop := context.AfterFunc(ctx, func() { _ = connection.Close() })
|
|
defer stop()
|
|
connection.SetReadLimit(96 * 1024)
|
|
_ = connection.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
|
_ = connection.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
if err = connection.WriteJSON(request); err != nil {
|
|
connection.Close()
|
|
return nil, err
|
|
}
|
|
var response sharedFrame
|
|
if err = connection.ReadJSON(&response); err != nil {
|
|
connection.Close()
|
|
return nil, err
|
|
}
|
|
if response.Error != "" || response.Type != "attached" {
|
|
connection.Close()
|
|
if response.Error == ErrAttachmentLimit.Error() {
|
|
return nil, ErrAttachmentLimit
|
|
}
|
|
return nil, errors.New("shared stream attachment rejected: " + response.Error)
|
|
}
|
|
_ = connection.SetReadDeadline(time.Time{})
|
|
attachment := &remoteAttachment{connection: connection, queue: newByteQueue(defaultAttachmentQueueBytes), acknowledgements: make(chan error, 1), done: make(chan struct{})}
|
|
go attachment.read()
|
|
return attachment, nil
|
|
}
|
|
|
|
type remoteAttachment struct {
|
|
connection *websocket.Conn
|
|
queue *byteQueue
|
|
requestMu sync.Mutex
|
|
closeOnce sync.Once
|
|
acknowledgements chan error
|
|
done chan struct{}
|
|
}
|
|
|
|
func (a *remoteAttachment) read() {
|
|
defer a.Close()
|
|
for {
|
|
var frame sharedFrame
|
|
if a.connection.ReadJSON(&frame) != nil {
|
|
return
|
|
}
|
|
switch frame.Type {
|
|
case "data":
|
|
if !a.queue.push(frame.Data) {
|
|
return
|
|
}
|
|
case "ack":
|
|
var err error
|
|
if frame.Error != "" {
|
|
err = errors.New(frame.Error)
|
|
}
|
|
select {
|
|
case a.acknowledgements <- err:
|
|
default:
|
|
return
|
|
}
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *remoteAttachment) Receive(ctx context.Context) ([]byte, error) { return a.queue.pop(ctx) }
|
|
func (a *remoteAttachment) Write(ctx context.Context, payload []byte) error {
|
|
if len(payload) == 0 || len(payload) > MaxDataFrameBytes {
|
|
return ErrInvalidDataFrame
|
|
}
|
|
return a.command(ctx, sharedFrame{Type: "write", Data: payload})
|
|
}
|
|
func (a *remoteAttachment) SendBreak(ctx context.Context) error {
|
|
return a.command(ctx, sharedFrame{Type: "break"})
|
|
}
|
|
func (a *remoteAttachment) command(ctx context.Context, frame sharedFrame) error {
|
|
a.requestMu.Lock()
|
|
defer a.requestMu.Unlock()
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
deadline := time.Now().Add(10 * time.Second)
|
|
if value, ok := ctx.Deadline(); ok && value.Before(deadline) {
|
|
deadline = value
|
|
}
|
|
_ = a.connection.SetWriteDeadline(deadline)
|
|
if err := a.connection.WriteJSON(frame); err != nil {
|
|
a.Close()
|
|
return err
|
|
}
|
|
timer := time.NewTimer(time.Until(deadline))
|
|
defer timer.Stop()
|
|
select {
|
|
case err := <-a.acknowledgements:
|
|
return err
|
|
case <-a.done:
|
|
return ErrChannelClosed
|
|
case <-ctx.Done():
|
|
a.Close()
|
|
return ctx.Err()
|
|
case <-timer.C:
|
|
a.Close()
|
|
return context.DeadlineExceeded
|
|
}
|
|
}
|
|
func (a *remoteAttachment) Close() error {
|
|
a.closeOnce.Do(func() {
|
|
close(a.done)
|
|
_ = a.connection.Close()
|
|
a.queue.close(ErrChannelClosed)
|
|
})
|
|
return nil
|
|
}
|
|
|