WIP: legacy WS.

This commit is contained in:
Miroslav Šedivý 2024-07-19 19:10:27 +02:00
parent 5527642878
commit e4c0c68d79
9 changed files with 747 additions and 1 deletions

View File

@ -34,7 +34,7 @@ export class WebsocketReconnector extends ReconnectorAbstract {
} }
let url = this._state.url let url = this._state.url
url = url.replace(/^http/, 'ws').replace(/\/+$/, '') + '/api/ws' url = url.replace(/^http/, 'ws').replace(/\/+$/, '') + '/ws'
const token = this._state.token const token = this._state.token
if (token) { if (token) {

View File

@ -16,6 +16,11 @@ export default defineConfig({
server: { server: {
port: 3001, port: 3001,
proxy: process.env.NEKO_HOST ? { proxy: process.env.NEKO_HOST ? {
'/ws': {
target: 'http://' + process.env.NEKO_HOST + ':' + process.env.NEKO_PORT + '/',
changeOrigin: true,
ws: true
},
'/api': { '/api': {
target: 'http://' + process.env.NEKO_HOST + ':' + process.env.NEKO_PORT + '/', target: 'http://' + process.env.NEKO_HOST + ':' + process.env.NEKO_PORT + '/',
changeOrigin: true, changeOrigin: true,

View File

@ -0,0 +1,64 @@
package event
const (
SYSTEM_INIT = "system/init"
SYSTEM_DISCONNECT = "system/disconnect"
SYSTEM_ERROR = "system/error"
)
const (
SIGNAL_OFFER = "signal/offer"
SIGNAL_ANSWER = "signal/answer"
SIGNAL_PROVIDE = "signal/provide"
SIGNAL_CANDIDATE = "signal/candidate"
)
const (
MEMBER_LIST = "member/list"
MEMBER_CONNECTED = "member/connected"
MEMBER_DISCONNECTED = "member/disconnected"
)
const (
CONTROL_LOCKED = "control/locked"
CONTROL_RELEASE = "control/release"
CONTROL_REQUEST = "control/request"
CONTROL_REQUESTING = "control/requesting"
CONTROL_GIVE = "control/give"
CONTROL_CLIPBOARD = "control/clipboard"
CONTROL_KEYBOARD = "control/keyboard"
)
const (
CHAT_MESSAGE = "chat/message"
CHAT_EMOTE = "chat/emote"
)
const (
FILETRANSFER_LIST = "filetransfer/list"
FILETRANSFER_REFRESH = "filetransfer/refresh"
)
const (
SCREEN_CONFIGURATIONS = "screen/configurations"
SCREEN_RESOLUTION = "screen/resolution"
SCREEN_SET = "screen/set"
)
const (
BROADCAST_STATUS = "broadcast/status"
BROADCAST_CREATE = "broadcast/create"
BROADCAST_DESTROY = "broadcast/destroy"
)
const (
ADMIN_BAN = "admin/ban"
ADMIN_KICK = "admin/kick"
ADMIN_LOCK = "admin/lock"
ADMIN_MUTE = "admin/mute"
ADMIN_UNLOCK = "admin/unlock"
ADMIN_UNMUTE = "admin/unmute"
ADMIN_CONTROL = "admin/control"
ADMIN_RELEASE = "admin/release"
ADMIN_GIVE = "admin/give"
)

View File

@ -0,0 +1,175 @@
package legacy
import (
"fmt"
"log"
"net/http"
"github.com/demodesk/neko/pkg/types"
"github.com/demodesk/neko/pkg/utils"
"github.com/gorilla/websocket"
)
var (
// DefaultUpgrader specifies the parameters for upgrading an HTTP
// connection to a WebSocket connection.
DefaultUpgrader = &websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// DefaultDialer is a dialer with all fields set to the default zero values.
DefaultDialer = websocket.DefaultDialer
)
type LegacyHandler struct {
}
func New() *LegacyHandler {
// Init
return &LegacyHandler{}
}
func (h *LegacyHandler) Route(r types.Router) {
log.Println("legacy handler route")
r.Get("/ws", func(w http.ResponseWriter, r *http.Request) error {
connBackend, _, err := DefaultDialer.Dial("ws://127.0.0.1:8080/api/ws?token="+r.URL.Query().Get("token"), nil)
if err != nil {
return utils.HttpError(http.StatusServiceUnavailable).
WithInternalErr(err).
Msg("couldn't dial to remote backend url")
}
defer connBackend.Close()
connClient, err := DefaultUpgrader.Upgrade(w, r, nil)
if err != nil {
return utils.HttpError(http.StatusInternalServerError).
WithInternalErr(err).
Msg("couldn't upgrade connection to websocket")
}
defer connClient.Close()
errClient := make(chan error, 1)
errBackend := make(chan error, 1)
replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error, rewriteTextMessage func([]byte) ([]byte, error)) {
for {
msgType, msg, err := src.ReadMessage()
if err != nil {
m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err))
if e, ok := err.(*websocket.CloseError); ok {
if e.Code != websocket.CloseNoStatusReceived {
m = websocket.FormatCloseMessage(e.Code, e.Text)
}
}
errc <- err
dst.WriteMessage(websocket.CloseMessage, m)
break
}
if msgType == websocket.TextMessage {
msg, err = rewriteTextMessage(msg)
if err != nil {
log.Printf("websocketproxy: Error when rewriting message: %v", err)
continue
}
}
err = dst.WriteMessage(msgType, msg)
if err != nil {
errc <- err
break
}
}
}
// client -> backend
go replicateWebsocketConn(connClient, connBackend, errClient, h.wsToBackend)
// backend -> client
go replicateWebsocketConn(connBackend, connClient, errBackend, h.wsToClient)
var message string
select {
case err = <-errClient:
message = "websocketproxy: Error when copying from backend to client: %v"
case err = <-errBackend:
message = "websocketproxy: Error when copying from client to backend: %v"
}
if e, ok := err.(*websocket.CloseError); !ok || e.Code == websocket.CloseAbnormalClosure {
log.Printf(message, err)
}
return nil
})
/*
r.Get("/stats", func(w http.ResponseWriter, r *http.Request) error {
password := r.URL.Query().Get("pwd")
isAdmin, err := webSocketHandler.IsAdmin(password)
if err != nil {
return utils.HttpForbidden(err)
}
if !isAdmin {
return utils.HttpUnauthorized().Msg("bad authorization")
}
w.Header().Set("Content-Type", "application/json")
stats := webSocketHandler.Stats()
return json.NewEncoder(w).Encode(stats)
})
r.Get("/screenshot.jpg", func(w http.ResponseWriter, r *http.Request) error {
password := r.URL.Query().Get("pwd")
isAdmin, err := webSocketHandler.IsAdmin(password)
if err != nil {
return utils.HttpForbidden(err)
}
if !isAdmin {
return utils.HttpUnauthorized().Msg("bad authorization")
}
if webSocketHandler.IsLocked("login") {
return utils.HttpError(http.StatusLocked).Msg("room is locked")
}
quality, err := strconv.Atoi(r.URL.Query().Get("quality"))
if err != nil {
quality = 90
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Content-Type", "image/jpeg")
img := desktop.GetScreenshotImage()
if err := jpeg.Encode(w, img, &jpeg.Options{Quality: quality}); err != nil {
return utils.HttpInternalServerError().WithInternalErr(err)
}
return nil
})
// allow downloading and uploading files
if webSocketHandler.FileTransferEnabled() {
r.Get("/file", func(w http.ResponseWriter, r *http.Request) error {
return nil
})
r.Post("/file", func(w http.ResponseWriter, r *http.Request) error {
return nil
})
}
*/
r.Get("/health", func(w http.ResponseWriter, r *http.Request) error {
_, err := w.Write([]byte("true"))
return err
})
}

View File

@ -0,0 +1,155 @@
package message
import (
"github.com/demodesk/neko/internal/http/legacy/types"
"github.com/pion/webrtc/v3"
)
type Message struct {
Event string `json:"event"`
}
type SystemInit struct {
Event string `json:"event"`
ImplicitHosting bool `json:"implicit_hosting"`
Locks map[string]string `json:"locks"`
FileTransfer bool `json:"file_transfer"`
}
type SystemMessage struct {
Event string `json:"event"`
Title string `json:"title"`
Message string `json:"message"`
}
type SignalProvide struct {
Event string `json:"event"`
ID string `json:"id"`
SDP string `json:"sdp"`
Lite bool `json:"lite"`
ICE []webrtc.ICEServer `json:"ice"`
}
type SignalOffer struct {
Event string `json:"event"`
SDP string `json:"sdp"`
}
type SignalAnswer struct {
Event string `json:"event"`
DisplayName string `json:"displayname"`
SDP string `json:"sdp"`
}
type SignalCandidate struct {
Event string `json:"event"`
Data string `json:"data"`
}
type MembersList struct {
Event string `json:"event"`
Members []*types.Member `json:"members"`
}
type Member struct {
Event string `json:"event"`
*types.Member
}
type MemberDisconnected struct {
Event string `json:"event"`
ID string `json:"id"`
}
type Clipboard struct {
Event string `json:"event"`
Text string `json:"text"`
}
type Keyboard struct {
Event string `json:"event"`
Layout *string `json:"layout,omitempty"`
CapsLock *bool `json:"capsLock,omitempty"`
NumLock *bool `json:"numLock,omitempty"`
ScrollLock *bool `json:"scrollLock,omitempty"` // TODO: ScrollLock is deprecated.
}
type Control struct {
Event string `json:"event"`
ID string `json:"id"`
}
type ControlTarget struct {
Event string `json:"event"`
ID string `json:"id"`
Target string `json:"target"`
}
type ChatReceive struct {
Event string `json:"event"`
Content string `json:"content"`
}
type ChatSend struct {
Event string `json:"event"`
ID string `json:"id"`
Content string `json:"content"`
}
type EmoteReceive struct {
Event string `json:"event"`
Emote string `json:"emote"`
}
type EmoteSend struct {
Event string `json:"event"`
ID string `json:"id"`
Emote string `json:"emote"`
}
type FileTransferList struct {
Event string `json:"event"`
Cwd string `json:"cwd"`
Files []types.FileListItem `json:"files"`
}
type Admin struct {
Event string `json:"event"`
ID string `json:"id"`
}
type AdminTarget struct {
Event string `json:"event"`
Target string `json:"target"`
ID string `json:"id"`
}
type AdminLock struct {
Event string `json:"event"`
Resource string `json:"resource"`
ID string `json:"id"`
}
type ScreenResolution struct {
Event string `json:"event"`
ID string `json:"id,omitempty"`
Width int `json:"width"`
Height int `json:"height"`
Rate int16 `json:"rate"`
}
type ScreenConfigurations struct {
Event string `json:"event"`
Configurations map[int]types.ScreenConfiguration `json:"configurations"`
}
type BroadcastStatus struct {
Event string `json:"event"`
URL string `json:"url"`
IsActive bool `json:"isActive"`
}
type BroadcastCreate struct {
Event string `json:"event"`
URL string `json:"url"`
}

View File

@ -0,0 +1,20 @@
package types
type Member struct {
ID string `json:"id"`
Name string `json:"displayname"`
Admin bool `json:"admin"`
Muted bool `json:"muted"`
}
type FileListItem struct {
Filename string `json:"name"`
Type string `json:"type"`
Size int64 `json:"size"`
}
type ScreenConfiguration struct {
Width int `json:"width"`
Height int `json:"height"`
Rates map[int]int16 `json:"rates"`
}

View File

@ -0,0 +1,161 @@
package legacy
import (
"encoding/json"
"fmt"
"github.com/demodesk/neko/internal/http/legacy/event"
"github.com/demodesk/neko/internal/http/legacy/message"
)
func (h *LegacyHandler) wsToBackend(msg []byte) ([]byte, error) {
header := message.Message{}
err := json.Unmarshal(msg, &header)
if err != nil {
return nil, err
}
var response any
switch header.Event {
// Signal Events
case event.SIGNAL_OFFER:
request := &message.SignalOffer{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
case event.SIGNAL_ANSWER:
request := &message.SignalAnswer{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
case event.SIGNAL_CANDIDATE:
request := &message.SignalCandidate{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
// Control Events
case event.CONTROL_RELEASE:
case event.CONTROL_REQUEST:
case event.CONTROL_GIVE:
request := &message.Control{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
case event.CONTROL_CLIPBOARD:
request := &message.Clipboard{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
case event.CONTROL_KEYBOARD:
request := &message.Keyboard{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
// Chat Events
case event.CHAT_MESSAGE:
request := &message.ChatReceive{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
case event.CHAT_EMOTE:
request := &message.EmoteReceive{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
// File Transfer Events
case event.FILETRANSFER_REFRESH:
// Screen Events
case event.SCREEN_RESOLUTION:
case event.SCREEN_CONFIGURATIONS:
case event.SCREEN_SET:
request := &message.ScreenResolution{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
// Broadcast Events
case event.BROADCAST_CREATE:
request := &message.BroadcastCreate{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
case event.BROADCAST_DESTROY:
// Admin Events
case event.ADMIN_LOCK:
request := &message.AdminLock{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
case event.ADMIN_UNLOCK:
request := &message.AdminLock{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
case event.ADMIN_CONTROL:
case event.ADMIN_RELEASE:
case event.ADMIN_GIVE:
request := &message.Admin{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
case event.ADMIN_BAN:
request := &message.Admin{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
case event.ADMIN_KICK:
request := &message.Admin{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
case event.ADMIN_MUTE:
request := &message.Admin{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
case event.ADMIN_UNMUTE:
request := &message.Admin{}
err := json.Unmarshal(msg, request)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown event type: %s", header.Event)
}
return json.Marshal(request)
}

View File

@ -0,0 +1,162 @@
package legacy
import (
"encoding/json"
"fmt"
"github.com/demodesk/neko/internal/http/legacy/event"
"github.com/demodesk/neko/internal/http/legacy/message"
)
func (h *LegacyHandler) wsToClient(msg []byte) ([]byte, error) {
header := message.Message{}
err := json.Unmarshal(msg, &header)
if err != nil {
return nil, err
}
var payload any
switch header.Event {
// System Events
case:
payload = &message.SystemMessage{
Event: event.SYSTEM_DISCONNECT,
}
case:
payload = &message.SystemMessage{
Event: event.SYSTEM_ERROR,
}
case:
payload = &message.SystemInit{
Event: event.SYSTEM_INIT,
}
// Member Events
case:
payload = &message.MembersList{
Event: event.MEMBER_LIST,
}
case:
payload = &message.Member{
Event: event.MEMBER_CONNECTED,
}
case:
payload = &message.MemberDisconnected{
Event: event.MEMBER_DISCONNECTED,
}
// Signal Events
case:
payload = &message.SignalOffer{
Event: event.SIGNAL_OFFER,
}
case:
payload = &message.SignalAnswer{
Event: event.SIGNAL_ANSWER,
}
case:
payload = &message.SignalCandidate{
Event: event.SIGNAL_CANDIDATE,
}
case:
payload = &message.SignalProvide{
Event: event.SIGNAL_PROVIDE,
}
// Control Events
case:
payload = &message.Clipboard{
Event: event.CONTROL_CLIPBOARD,
}
case:
payload = &message.Control{
Event: event.CONTROL_REQUEST,
}
case:
payload = &message.Control{
Event: event.CONTROL_REQUESTING,
}
case:
payload = &message.ControlTarget{
Event: event.CONTROL_GIVE,
} // message.AdminTarget
case:
payload = &message.Control{
Event: event.CONTROL_RELEASE,
}
case:
payload = &message.Control{
Event: event.CONTROL_LOCKED,
}
// Chat Events
case:
payload = &message.ChatSend{
Event: event.CHAT_MESSAGE,
}
case:
payload = &message.EmoteSend{
Event: event.CHAT_EMOTE,
}
// File Transfer Events
case:
payload = &message.FileTransferList{
Event: event.FILETRANSFER_LIST,
}
// Screen Events
case:
payload = &message.ScreenResolution{
Event: event.SCREEN_RESOLUTION,
}
case:
payload = &message.ScreenConfigurations{
Event: event.SCREEN_CONFIGURATIONS,
}
// Broadcast Events
case:
payload = &message.BroadcastStatus{
Event: event.BROADCAST_STATUS,
}
// Admin Events
case:
payload = &message.AdminLock{
Event: event.ADMIN_UNLOCK,
}
case:
payload = &message.AdminLock{
Event: event.ADMIN_LOCK,
}
case:
payload = &message.AdminTarget{
Event: event.ADMIN_CONTROL,
} // message.Admin
case:
payload = &message.AdminTarget{
Event: event.ADMIN_RELEASE,
} // message.Admin
case:
payload = &message.AdminTarget{
Event: event.ADMIN_MUTE,
}
case:
payload = &message.AdminTarget{
Event: event.ADMIN_UNMUTE,
}
case:
payload = &message.AdminTarget{
Event: event.ADMIN_KICK,
}
case:
payload = &message.AdminTarget{
Event: event.ADMIN_BAN,
}
default:
return nil, fmt.Errorf("unknown event type: %s", header.Event)
}
return json.Marshal(payload)
}

View File

@ -10,6 +10,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/demodesk/neko/internal/config" "github.com/demodesk/neko/internal/config"
"github.com/demodesk/neko/internal/http/legacy"
"github.com/demodesk/neko/pkg/types" "github.com/demodesk/neko/pkg/types"
) )
@ -54,6 +55,9 @@ func New(WebSocketManager types.WebSocketManager, ApiManager types.ApiManager, c
return config.AllowOrigin(r.Header.Get("Origin")) return config.AllowOrigin(r.Header.Get("Origin"))
})) }))
// Legacy handler
legacy.New().Route(router)
batch := batchHandler{ batch := batchHandler{
Router: router, Router: router,
PathPrefix: "/api", PathPrefix: "/api",