From e4c0c68d79ac4ea70f95b64dc35379aa7f9925c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Fri, 19 Jul 2024 19:10:27 +0200 Subject: [PATCH] WIP: legacy WS. --- .../internal/reconnector/websocket.ts | 2 +- client/vite.config.ts | 5 + server/internal/http/legacy/event/events.go | 64 +++++++ server/internal/http/legacy/handler.go | 175 ++++++++++++++++++ .../internal/http/legacy/message/messages.go | 155 ++++++++++++++++ server/internal/http/legacy/types/types.go | 20 ++ server/internal/http/legacy/wstobackend.go | 161 ++++++++++++++++ server/internal/http/legacy/wstoclient.go | 162 ++++++++++++++++ server/internal/http/manager.go | 4 + 9 files changed, 747 insertions(+), 1 deletion(-) create mode 100644 server/internal/http/legacy/event/events.go create mode 100644 server/internal/http/legacy/handler.go create mode 100644 server/internal/http/legacy/message/messages.go create mode 100644 server/internal/http/legacy/types/types.go create mode 100644 server/internal/http/legacy/wstobackend.go create mode 100644 server/internal/http/legacy/wstoclient.go diff --git a/client/src/component/internal/reconnector/websocket.ts b/client/src/component/internal/reconnector/websocket.ts index fd09a06e..f1b356a8 100644 --- a/client/src/component/internal/reconnector/websocket.ts +++ b/client/src/component/internal/reconnector/websocket.ts @@ -34,7 +34,7 @@ export class WebsocketReconnector extends ReconnectorAbstract { } 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 if (token) { diff --git a/client/vite.config.ts b/client/vite.config.ts index acf37696..60aba53d 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -16,6 +16,11 @@ export default defineConfig({ server: { port: 3001, proxy: process.env.NEKO_HOST ? { + '/ws': { + target: 'http://' + process.env.NEKO_HOST + ':' + process.env.NEKO_PORT + '/', + changeOrigin: true, + ws: true + }, '/api': { target: 'http://' + process.env.NEKO_HOST + ':' + process.env.NEKO_PORT + '/', changeOrigin: true, diff --git a/server/internal/http/legacy/event/events.go b/server/internal/http/legacy/event/events.go new file mode 100644 index 00000000..8032ba90 --- /dev/null +++ b/server/internal/http/legacy/event/events.go @@ -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" +) diff --git a/server/internal/http/legacy/handler.go b/server/internal/http/legacy/handler.go new file mode 100644 index 00000000..31e08e8b --- /dev/null +++ b/server/internal/http/legacy/handler.go @@ -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 + }) +} diff --git a/server/internal/http/legacy/message/messages.go b/server/internal/http/legacy/message/messages.go new file mode 100644 index 00000000..7db65f45 --- /dev/null +++ b/server/internal/http/legacy/message/messages.go @@ -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"` +} diff --git a/server/internal/http/legacy/types/types.go b/server/internal/http/legacy/types/types.go new file mode 100644 index 00000000..eb05cbac --- /dev/null +++ b/server/internal/http/legacy/types/types.go @@ -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"` +} diff --git a/server/internal/http/legacy/wstobackend.go b/server/internal/http/legacy/wstobackend.go new file mode 100644 index 00000000..f50410b9 --- /dev/null +++ b/server/internal/http/legacy/wstobackend.go @@ -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) +} diff --git a/server/internal/http/legacy/wstoclient.go b/server/internal/http/legacy/wstoclient.go new file mode 100644 index 00000000..002651b8 --- /dev/null +++ b/server/internal/http/legacy/wstoclient.go @@ -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) +} diff --git a/server/internal/http/manager.go b/server/internal/http/manager.go index 4a753314..fdb630b7 100644 --- a/server/internal/http/manager.go +++ b/server/internal/http/manager.go @@ -10,6 +10,7 @@ import ( "github.com/rs/zerolog/log" "github.com/demodesk/neko/internal/config" + "github.com/demodesk/neko/internal/http/legacy" "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")) })) + // Legacy handler + legacy.New().Route(router) + batch := batchHandler{ Router: router, PathPrefix: "/api",