neko/server/internal/http/legacy/handler.go
2025-03-26 23:51:19 +01:00

402 lines
10 KiB
Go

package legacy
import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"github.com/m1k1o/neko/server/internal/api"
oldEvent "github.com/m1k1o/neko/server/internal/http/legacy/event"
oldMessage "github.com/m1k1o/neko/server/internal/http/legacy/message"
oldTypes "github.com/m1k1o/neko/server/internal/http/legacy/types"
"github.com/m1k1o/neko/server/pkg/types"
"github.com/m1k1o/neko/server/pkg/types/event"
"github.com/m1k1o/neko/server/pkg/types/message"
"github.com/m1k1o/neko/server/pkg/utils"
"github.com/gorilla/websocket"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
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 {
logger zerolog.Logger
serverAddr string
bannedIPs map[string]struct{}
sessionIPs map[string]string
}
func New() *LegacyHandler {
// Init
return &LegacyHandler{
logger: log.With().Str("module", "legacy").Logger(),
serverAddr: "127.0.0.1:8080",
bannedIPs: make(map[string]struct{}),
sessionIPs: make(map[string]string),
}
}
func (h *LegacyHandler) Route(r types.Router) {
r.Get("/ws", func(w http.ResponseWriter, r *http.Request) error {
s := h.newSession(r)
// create a new websocket connection
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()
s.connClient = connClient
if h.isBanned(r) {
s.toClient(&oldMessage.SystemMessage{
Event: oldEvent.SYSTEM_DISCONNECT,
Title: "banned ip",
Message: "you are banned",
})
}
// create a new session
username := r.URL.Query().Get("username")
password := r.URL.Query().Get("password")
err = s.create(username, password)
if err != nil {
h.logger.Error().Err(err).Msg("couldn't create a new session")
s.toClient(&oldMessage.SystemMessage{
Event: oldEvent.SYSTEM_DISCONNECT,
Title: "couldn't create a new session",
Message: err.Error(),
})
// we can't return HTTP error here because the connection is already upgraded
return nil
}
defer s.destroy()
// dial to the remote backend
connBackend, _, err := DefaultDialer.Dial("ws://"+h.serverAddr+"/api/ws?token="+url.QueryEscape(s.token), nil)
if err != nil {
h.logger.Error().Err(err).Msg("couldn't dial to the remote backend")
s.toClient(&oldMessage.SystemMessage{
Event: oldEvent.SYSTEM_DISCONNECT,
Title: "couldn't dial to the remote backend",
Message: err.Error(),
})
// we can't return HTTP error here because the connection is already upgraded
return nil
}
defer connBackend.Close()
s.connBackend = connBackend
// request signal
if err = s.toBackend(event.SIGNAL_REQUEST, message.SignalRequest{}); err != nil {
h.logger.Error().Err(err).Msg("couldn't request signal")
s.toClient(&oldMessage.SystemMessage{
Event: oldEvent.SYSTEM_DISCONNECT,
Title: "couldn't request signal",
Message: err.Error(),
})
// we can't return HTTP error here because the connection is already upgraded
return nil
}
// copy messages between the client and the backend
errClient := make(chan error, 1)
errBackend := make(chan error, 1)
replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error, rewriteTextMessage func([]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 {
err = rewriteTextMessage(msg)
if err == nil {
continue
}
if errors.Is(err, ErrBackendRespone) {
h.logger.Error().Err(err).Msg("backend response error")
s.toClient(&oldMessage.SystemMessage{
Event: oldEvent.SYSTEM_ERROR,
Title: "backend response error",
Message: strings.ReplaceAll(err.Error(), ErrBackendRespone.Error()+": ", ""),
})
continue
} else if errors.Is(err, ErrWebsocketSend) {
errc <- err
break
} else {
h.logger.Error().Err(err).Msg("couldn't rewrite text message")
}
}
}
}
// backend -> client
go replicateWebsocketConn(connClient, connBackend, errClient, s.wsToClient)
// client -> backend
go replicateWebsocketConn(connBackend, connClient, errBackend, s.wsToBackend)
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 {
h.logger.Error().Err(err).Msg(message)
}
return nil
})
r.Get("/stats", func(w http.ResponseWriter, r *http.Request) error {
if h.isBanned(r) {
return utils.HttpForbidden("banned ip")
}
s := h.newSession(r)
// create a new session
username := r.URL.Query().Get("usr")
password := r.URL.Query().Get("pwd")
err := s.create(username, password)
if err != nil {
return utils.HttpForbidden(err.Error())
}
defer s.destroy()
if !s.isAdmin {
return utils.HttpUnauthorized().Msg("bad authorization")
}
w.Header().Set("Content-Type", "application/json")
// get all sessions
sessions := []api.SessionDataPayload{}
err = s.apiReq(http.MethodGet, "/api/sessions", nil, &sessions)
if err != nil {
return utils.HttpInternalServerError().WithInternalErr(err)
}
// get stats
newStats := types.Stats{}
err = s.apiReq(http.MethodGet, "/api/stats", nil, &newStats)
if err != nil {
return utils.HttpInternalServerError().WithInternalErr(err)
}
// get settings
settings := types.Settings{}
err = s.apiReq(http.MethodGet, "/api/room/settings", nil, &settings)
if err != nil {
return utils.HttpInternalServerError().WithInternalErr(err)
}
var stats oldTypes.Stats
// create empty array so that it's not null in json
stats.Members = []*oldTypes.Member{}
for _, session := range sessions {
if session.State.IsConnected {
stats.Connections++
member, err := profileToMember(session.ID, session.Profile)
if err != nil {
return utils.HttpInternalServerError().WithInternalErr(err)
}
// append members
stats.Members = append(stats.Members, member)
}
}
locks, err := s.settingsToLocks(settings)
if err != nil {
return err
}
stats.Host = newStats.HostId
// TODO: stats.Banned, not implemented yet
stats.Locked = locks
stats.ServerStartedAt = newStats.ServerStartedAt
stats.LastAdminLeftAt = newStats.LastAdminLeftAt
stats.LastUserLeftAt = newStats.LastUserLeftAt
stats.ControlProtection = settings.ControlProtection
stats.ImplicitControl = settings.ImplicitHosting
return json.NewEncoder(w).Encode(stats)
})
r.Get("/screenshot.jpg", func(w http.ResponseWriter, r *http.Request) error {
if h.isBanned(r) {
return utils.HttpForbidden("banned ip")
}
s := h.newSession(r)
// create a new session
username := r.URL.Query().Get("usr")
password := r.URL.Query().Get("pwd")
err := s.create(username, password)
if err != nil {
return utils.HttpForbidden(err.Error())
}
defer s.destroy()
if !s.isAdmin {
return utils.HttpUnauthorized().Msg("bad authorization")
}
quality := r.URL.Query().Get("quality")
// get the screenshot
body, headers, err := s.req(http.MethodGet, "/api/room/screen/shot.jpg?quality="+url.QueryEscape(quality), nil, nil)
if err != nil {
return utils.HttpInternalServerError().WithInternalErr(err)
}
// copy headers
w.Header().Set("Content-Length", headers.Get("Content-Length"))
w.Header().Set("Content-Type", headers.Get("Content-Type"))
// copy the body to the response writer
_, err = io.Copy(w, body)
return err
})
// allow downloading and uploading files
r.Get("/file", func(w http.ResponseWriter, r *http.Request) error {
if h.isBanned(r) {
return utils.HttpForbidden("banned ip")
}
s := h.newSession(r)
// create a new session
username := r.URL.Query().Get("usr")
password := r.URL.Query().Get("pwd")
err := s.create(username, password)
if err != nil {
return utils.HttpForbidden(err.Error())
}
defer s.destroy()
filename := r.URL.Query().Get("filename")
body, headers, err := s.req(http.MethodGet, "/api/filetransfer?filename="+url.QueryEscape(filename), r.Header, nil)
if err != nil {
return utils.HttpInternalServerError().WithInternalErr(err)
}
// copy headers
w.Header().Set("Content-Length", headers.Get("Content-Length"))
w.Header().Set("Content-Type", headers.Get("Content-Type"))
// copy the body to the response writer
_, err = io.Copy(w, body)
return err
})
r.Post("/file", func(w http.ResponseWriter, r *http.Request) error {
if h.isBanned(r) {
return utils.HttpForbidden("banned ip")
}
s := h.newSession(r)
// create a new session
username := r.URL.Query().Get("usr")
password := r.URL.Query().Get("pwd")
err := s.create(username, password)
if err != nil {
return utils.HttpForbidden(err.Error())
}
defer s.destroy()
body, _, err := s.req(http.MethodPost, "/api/filetransfer", r.Header, r.Body)
if err != nil {
return utils.HttpInternalServerError().WithInternalErr(err)
}
// copy the body to the response writer
_, err = io.Copy(w, body)
return err
})
r.Get("/health", func(w http.ResponseWriter, r *http.Request) error {
_, err := w.Write([]byte("true"))
return err
})
}
func (h *LegacyHandler) ban(sessionId string) error {
// find session by id
ip, ok := h.sessionIPs[sessionId]
if !ok {
return fmt.Errorf("session not found")
}
h.bannedIPs[ip] = struct{}{}
return nil
}
func (h *LegacyHandler) isBanned(r *http.Request) bool {
ip := getIp(r)
_, ok := h.bannedIPs[ip]
return ok
}
func getIp(r *http.Request) string {
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
if e, ok := err.(*net.AddrError); ok && e.Err == "missing port in address" {
return r.RemoteAddr
}
return ""
}
return ip
}