move filetransfer to locks.

This commit is contained in:
Miroslav Šedivý
2022-11-19 20:26:45 +01:00
parent cdb9b185f2
commit d17a7e8d82
33 changed files with 377 additions and 405 deletions

View File

@ -15,9 +15,8 @@ type WebSocket struct {
ControlProtection bool
FileTransfer bool
UnprivFileTransfer bool
FileTransferPath string
FileTransferEnabled bool
FileTransferPath string
}
func (WebSocket) Init(cmd *cobra.Command) error {
@ -46,13 +45,10 @@ func (WebSocket) Init(cmd *cobra.Command) error {
return err
}
cmd.PersistentFlags().Bool("file_transfer", false, "allow file transfer for admins")
if err := viper.BindPFlag("file_transfer", cmd.PersistentFlags().Lookup("file_transfer")); err != nil {
return err
}
// File transfer
cmd.PersistentFlags().Bool("unpriv_file_transfer", false, "allow file transfer for non admins")
if err := viper.BindPFlag("unpriv_file_transfer", cmd.PersistentFlags().Lookup("unpriv_file_transfer")); err != nil {
cmd.PersistentFlags().Bool("file_transfer_enabled", true, "enable file transfer feature")
if err := viper.BindPFlag("file_transfer_enabled", cmd.PersistentFlags().Lookup("file_transfer_enabled")); err != nil {
return err
}
@ -72,8 +68,7 @@ func (s *WebSocket) Set() {
s.ControlProtection = viper.GetBool("control_protection")
s.FileTransfer = viper.GetBool("file_transfer")
s.UnprivFileTransfer = viper.GetBool("unpriv_file_transfer")
s.FileTransferEnabled = viper.GetBool("file_transfer_enabled")
s.FileTransferPath = viper.GetString("file_transfer_path")
s.FileTransferPath = filepath.Clean(s.FileTransferPath)
}

View File

@ -105,70 +105,77 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop t
}
})
router.Get("/file", func(w http.ResponseWriter, r *http.Request) {
password := r.URL.Query().Get("pwd")
isAuthorized, err := webSocketHandler.CanTransferFiles(password)
if err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !isAuthorized {
http.Error(w, "bad authorization", http.StatusUnauthorized)
return
}
filename := r.URL.Query().Get("filename")
badChars, _ := regexp.MatchString(`(?m)\.\.(?:\/|$)`, filename)
if filename == "" || badChars {
http.Error(w, "bad filename", http.StatusBadRequest)
return
}
path := webSocketHandler.MakeFilePath(filename)
f, err := os.Open(path)
if err != nil {
http.Error(w, "not found or unable to open", http.StatusNotFound)
return
}
defer f.Close()
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
io.Copy(w, f)
})
router.Post("/file", func(w http.ResponseWriter, r *http.Request) {
password := r.URL.Query().Get("pwd")
isAuthorized, err := webSocketHandler.CanTransferFiles(password)
if err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !isAuthorized {
http.Error(w, "bad authorization", http.StatusUnauthorized)
return
}
r.ParseMultipartForm(32 << 20)
for _, formheader := range r.MultipartForm.File["files"] {
formfile, err := formheader.Open()
// allow downloading and uploading files
if webSocketHandler.FileTransferEnabled() {
router.Get("/file", func(w http.ResponseWriter, r *http.Request) {
password := r.URL.Query().Get("pwd")
isAuthorized, err := webSocketHandler.CanTransferFiles(password)
if err != nil {
logger.Warn().Err(err).Msg("failed to open formdata file")
http.Error(w, "error writing file", http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusForbidden)
return
}
defer formfile.Close()
f, err := os.OpenFile(webSocketHandler.MakeFilePath(formheader.Filename), os.O_WRONLY|os.O_CREATE, 0644)
if !isAuthorized {
http.Error(w, "bad authorization", http.StatusUnauthorized)
return
}
filename := r.URL.Query().Get("filename")
badChars, _ := regexp.MatchString(`(?m)\.\.(?:\/|$)`, filename)
if filename == "" || badChars {
http.Error(w, "bad filename", http.StatusBadRequest)
return
}
filePath := webSocketHandler.FileTransferPath(filename)
f, err := os.Open(filePath)
if err != nil {
http.Error(w, "unable to open file for writing", http.StatusInternalServerError)
http.Error(w, "not found or unable to open", http.StatusNotFound)
return
}
defer f.Close()
io.Copy(f, formfile)
}
})
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
io.Copy(w, f)
})
router.Post("/file", func(w http.ResponseWriter, r *http.Request) {
password := r.URL.Query().Get("pwd")
isAuthorized, err := webSocketHandler.CanTransferFiles(password)
if err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !isAuthorized {
http.Error(w, "bad authorization", http.StatusUnauthorized)
return
}
r.ParseMultipartForm(32 << 20)
for _, formheader := range r.MultipartForm.File["files"] {
filePath := webSocketHandler.FileTransferPath(formheader.Filename)
formfile, err := formheader.Open()
if err != nil {
logger.Warn().Err(err).Msg("failed to open formdata file")
http.Error(w, "error writing file", http.StatusInternalServerError)
return
}
defer formfile.Close()
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
http.Error(w, "unable to open file for writing", http.StatusInternalServerError)
return
}
defer f.Close()
io.Copy(f, formfile)
}
})
}
router.Get("/health", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("true"))

View File

@ -35,7 +35,6 @@ const (
)
const (
FILETRANSFER_STATUS = "filetransfer/status"
FILETRANSFER_LIST = "filetransfer/list"
FILETRANSFER_REFRESH = "filetransfer/refresh"
)

View File

@ -14,6 +14,7 @@ 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 {
@ -47,8 +48,8 @@ type SignalCandidate struct {
}
type MembersList struct {
Event string `json:"event"`
Memebers []*types.Member `json:"members"`
Event string `json:"event"`
Members []*types.Member `json:"members"`
}
type Member struct {
@ -106,17 +107,7 @@ type EmoteSend struct {
Emote string `json:"emote"`
}
type FileTransferTarget struct {
Event string `json:"event"`
}
type FileTransferStatus struct {
Event string `json:"event"`
Admin bool `json:"admin"`
Unpriv bool `json:"unpriv"`
}
type FileList struct {
type FileTransferList struct {
Event string `json:"event"`
Cwd string `json:"cwd"`
Files []types.FileListItem `json:"files"`

View File

@ -34,8 +34,11 @@ type WebSocketHandler interface {
Stats() Stats
IsLocked(resource string) bool
IsAdmin(password string) (bool, error)
// File Transfer
CanTransferFiles(password string) (bool, error)
MakeFilePath(filename string) string
FileTransferPath(filename string) string
FileTransferEnabled() bool
}
type FileListItem struct {

View File

@ -19,7 +19,12 @@ func (h *MessageHandler) adminLock(id string, session types.Session, payload *me
return nil
}
if payload.Resource != "login" && payload.Resource != "control" {
// allow only known resources
switch payload.Resource {
case "login":
case "control":
case "file_transfer":
default:
h.logger.Debug().Msg("unknown lock resource")
return nil
}

View File

@ -1,62 +0,0 @@
package handler
import (
"errors"
"m1k1o/neko/internal/types"
"m1k1o/neko/internal/types/event"
"m1k1o/neko/internal/types/message"
"m1k1o/neko/internal/utils"
)
func (h *MessageHandler) setFileTransferStatus(session types.Session, payload *message.FileTransferStatus) error {
if !session.Admin() {
h.logger.Debug().Msg("user not admin")
return nil
}
h.state.SetFileTransferState(payload.Admin, payload.Unpriv)
err := h.sessions.Broadcast(message.FileTransferStatus{
Event: event.FILETRANSFER_STATUS,
Admin: payload.Admin,
Unpriv: payload.Admin && payload.Unpriv,
}, nil)
if err != nil {
return err
}
files, err := utils.ListFiles(h.state.FileTransferPath())
if err != nil {
return err
}
msg := message.FileList{
Event: event.FILETRANSFER_LIST,
Cwd: h.state.FileTransferPath(),
Files: files,
}
if payload.Unpriv {
return h.sessions.Broadcast(msg, nil)
} else {
return h.sessions.AdminBroadcast(msg, nil)
}
}
func (h *MessageHandler) refresh(session types.Session) error {
if !(h.state.FileTransferEnabled() && session.Admin() || h.state.UnprivFileTransferEnabled()) {
return errors.New(session.Member().Name + " tried to refresh file list when they can't")
}
files, err := utils.ListFiles(h.state.FileTransferPath())
if err != nil {
return err
}
return session.Send(
message.FileList{
Event: event.FILETRANSFER_LIST,
Cwd: h.state.FileTransferPath(),
Files: files,
})
}

View File

@ -0,0 +1,42 @@
package handler
import (
"m1k1o/neko/internal/types"
"m1k1o/neko/internal/types/event"
"m1k1o/neko/internal/types/message"
"m1k1o/neko/internal/utils"
)
func (h *MessageHandler) FileTransferRefresh(session types.Session) error {
fileTransferPath := h.state.FileTransferPath("") // root
// allow users only if file transfer is not locked
if session != nil && !(session.Admin() || !h.state.IsLocked("file_transfer")) {
h.logger.Debug().Msg("file transfer is locked for users")
return nil
}
files, err := utils.ListFiles(fileTransferPath)
if err != nil {
return err
}
message := message.FileTransferList{
Event: event.FILETRANSFER_LIST,
Cwd: fileTransferPath,
Files: files,
}
// send to just one user
if session != nil {
return session.Send(message)
}
// broadcast to all admins
if h.state.IsLocked("file_transfer") {
return h.sessions.AdminBroadcast(message, nil)
}
// broadcast to all users
return h.sessions.Broadcast(message, nil)
}

View File

@ -127,14 +127,8 @@ func (h *MessageHandler) Message(id string, raw []byte) error {
}), "%s failed", header.Event)
// File Transfer Events
case event.FILETRANSFER_STATUS:
payload := &message.FileTransferStatus{}
return errors.Wrapf(
utils.Unmarshal(payload, raw, func() error {
return h.setFileTransferStatus(session, payload)
}), "%s failed", header.Event)
case event.FILETRANSFER_REFRESH:
return errors.Wrapf(h.refresh(session), "%s failed", header.Event)
return errors.Wrapf(h.FileTransferRefresh(session), "%s failed", header.Event)
// Screen Events
case event.SCREEN_RESOLUTION:

View File

@ -17,6 +17,7 @@ func (h *MessageHandler) SessionCreated(id string, session types.Session) error
Event: event.SYSTEM_INIT,
ImplicitHosting: h.webrtc.ImplicitControl(),
Locks: h.state.AllLocked(),
FileTransfer: h.state.FileTransferEnabled(),
}); err != nil {
h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.SYSTEM_INIT)
return err
@ -34,14 +35,21 @@ func (h *MessageHandler) SessionCreated(id string, session types.Session) error
}
}
// send file list if file transfer is enabled
if h.state.FileTransferEnabled() && (session.Admin() || !h.state.IsLocked("file_transfer")) {
if err := h.FileTransferRefresh(session); err != nil {
return err
}
}
return nil
}
func (h *MessageHandler) SessionConnected(id string, session types.Session) error {
// send list of members to session
if err := session.Send(message.MembersList{
Event: event.MEMBER_LIST,
Memebers: h.sessions.Members(),
Event: event.MEMBER_LIST,
Members: h.sessions.Members(),
}); err != nil {
h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.MEMBER_LIST)
return err

View File

@ -1,22 +1,22 @@
package state
import "path/filepath"
type State struct {
banned map[string]string // IP -> session ID (that banned it)
locked map[string]string // resource name -> session ID (that locked it)
fileTransferEnabled bool // admins can transfer files
fileTransferUnprivEnabled bool // all users can transfer files
fileTransferPath string // path where files are located
fileTransferEnabled bool
fileTransferPath string // path where files are located
}
func New(fileTransferEnabled bool, fileTransferUnprivEnabled bool, fileTransferPath string) *State {
func New(fileTransferEnabled bool, fileTransferPath string) *State {
return &State{
banned: make(map[string]string),
locked: make(map[string]string),
fileTransferEnabled: fileTransferEnabled,
fileTransferUnprivEnabled: fileTransferUnprivEnabled,
fileTransferPath: fileTransferPath,
fileTransferEnabled: fileTransferEnabled,
fileTransferPath: fileTransferPath,
}
}
@ -68,21 +68,17 @@ func (s *State) AllLocked() map[string]string {
return s.locked
}
// File Transfer
// File transfer
func (s *State) FileTransferPath(filename string) string {
if filename == "" {
return s.fileTransferPath
}
cleanPath := filepath.Clean(filename)
return filepath.Join(s.fileTransferPath, cleanPath)
}
func (s *State) FileTransferEnabled() bool {
return s.fileTransferEnabled
}
func (s *State) UnprivFileTransferEnabled() bool {
return s.fileTransferUnprivEnabled
}
func (s *State) SetFileTransferState(admin bool, unpriv bool) {
s.fileTransferEnabled = admin
s.fileTransferUnprivEnabled = unpriv
}
func (s *State) FileTransferPath() string {
return s.fileTransferPath
}

View File

@ -4,7 +4,6 @@ import (
"fmt"
"net/http"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"
@ -28,7 +27,7 @@ const CONTROL_PROTECTION_SESSION = "by_control_protection"
func New(sessions types.SessionManager, desktop types.DesktopManager, capture types.CaptureManager, webrtc types.WebRTCManager, conf *config.WebSocket) *WebSocketHandler {
logger := log.With().Str("module", "websocket").Logger()
state := state.New(conf.FileTransfer, conf.UnprivFileTransfer, conf.FileTransferPath)
state := state.New(conf.FileTransferEnabled, conf.FileTransferPath)
// if control protection is enabled
if conf.ControlProtection {
@ -36,9 +35,12 @@ func New(sessions types.SessionManager, desktop types.DesktopManager, capture ty
logger.Info().Msgf("control locked on behalf of control protection")
}
if _, err := os.Stat(conf.FileTransferPath); os.IsNotExist(err) {
err = os.Mkdir(conf.FileTransferPath, os.ModePerm)
logger.Err(err).Msg("creating file transfer directory")
// create file transfer directory if not exists
if conf.FileTransferEnabled {
if _, err := os.Stat(conf.FileTransferPath); os.IsNotExist(err) {
err = os.Mkdir(conf.FileTransferPath, os.ModePerm)
logger.Err(err).Msg("creating file transfer directory")
}
}
// apply default locks
@ -132,32 +134,6 @@ func (ws *WebSocketHandler) Start() {
}
}
// send file list if necessary
if ws.state.FileTransferEnabled() && (session.Admin() || ws.state.UnprivFileTransferEnabled()) {
err := session.Send(
message.FileTransferStatus{
Event: event.FILETRANSFER_STATUS,
Admin: ws.state.FileTransferEnabled(),
Unpriv: ws.state.UnprivFileTransferEnabled(),
})
if err != nil {
ws.logger.Warn().Err(err).Msgf("file transfer status event has failed")
return
}
files, err := utils.ListFiles(ws.conf.FileTransferPath)
if err == nil {
if err := session.Send(
message.FileList{
Event: event.FILETRANSFER_LIST,
Cwd: ws.conf.FileTransferPath,
Files: files,
}); err != nil {
ws.logger.Warn().Err(err).Msg("file list event has failed")
}
}
}
// remove outdated stats
if session.Admin() {
ws.lastAdminLeftAt = nil
@ -222,32 +198,35 @@ func (ws *WebSocketHandler) Start() {
ws.logger.Err(err).Msg("sync clipboard")
})
// watch for file changes
watcher, err := fsnotify.NewWatcher()
if err != nil {
ws.logger.Err(err).Msg("unable to start file transfer dir watcher")
return
}
go func() {
for {
select {
case e, ok := <-watcher.Events:
if !ok {
ws.logger.Info().Msg("file transfer dir watcher closed")
return
}
if e.Has(fsnotify.Create) || e.Has(fsnotify.Remove) || e.Has(fsnotify.Rename) {
ws.sendFileTransferUpdate()
}
case err := <-watcher.Errors:
ws.logger.Err(err).Msg("error in file transfer dir watcher")
}
// watch for file changes and send file list if file transfer is enabled
if ws.conf.FileTransferEnabled {
watcher, err := fsnotify.NewWatcher()
if err != nil {
ws.logger.Err(err).Msg("unable to start file transfer dir watcher")
return
}
}()
if err := watcher.Add(ws.conf.FileTransferPath); err != nil {
ws.logger.Err(err).Msg("unable to add file transfer path to watcher")
go func() {
for {
select {
case e, ok := <-watcher.Events:
if !ok {
ws.logger.Info().Msg("file transfer dir watcher closed")
return
}
if e.Has(fsnotify.Create) || e.Has(fsnotify.Remove) || e.Has(fsnotify.Rename) {
ws.logger.Debug().Str("event", e.String()).Msg("file transfer dir watcher event")
ws.handler.FileTransferRefresh(nil)
}
case err := <-watcher.Errors:
ws.logger.Err(err).Msg("error in file transfer dir watcher")
}
}
}()
if err := watcher.Add(ws.conf.FileTransferPath); err != nil {
ws.logger.Err(err).Msg("unable to add file transfer path to watcher")
}
}
}
@ -376,52 +355,6 @@ func (ws *WebSocketHandler) IsAdmin(password string) (bool, error) {
return false, fmt.Errorf("invalid password")
}
func (ws *WebSocketHandler) CanTransferFiles(password string) (bool, error) {
if !ws.state.FileTransferEnabled() {
return false, nil
}
isAdmin, err := ws.IsAdmin(password)
if err != nil {
return false, err
}
return isAdmin || ws.state.UnprivFileTransferEnabled(), nil
}
func (ws *WebSocketHandler) MakeFilePath(filename string) string {
cleanPath := filepath.Clean(filename)
return filepath.Join(ws.conf.FileTransferPath, cleanPath)
}
func (ws *WebSocketHandler) sendFileTransferUpdate() {
if !ws.state.FileTransferEnabled() {
return
}
files, err := utils.ListFiles(ws.conf.FileTransferPath)
if err != nil {
ws.logger.Err(err).Msg("unable to ls file transfer path")
return
}
message := message.FileList{
Event: event.FILETRANSFER_LIST,
Cwd: ws.conf.FileTransferPath,
Files: files,
}
if ws.state.UnprivFileTransferEnabled() {
err = ws.sessions.Broadcast(message, nil)
} else {
err = ws.sessions.AdminBroadcast(message, nil)
}
if err != nil {
ws.logger.Err(err).Msg("unable to broadcast file list")
}
}
func (ws *WebSocketHandler) authenticate(r *http.Request) (bool, error) {
passwords, ok := r.URL.Query()["password"]
if !ok || len(passwords[0]) < 1 {
@ -492,3 +425,28 @@ func (ws *WebSocketHandler) handle(connection *websocket.Conn, id string) {
}
}
}
//
// File transfer
//
func (ws *WebSocketHandler) CanTransferFiles(password string) (bool, error) {
if !ws.conf.FileTransferEnabled {
return false, nil
}
isAdmin, err := ws.IsAdmin(password)
if err != nil {
return false, err
}
return isAdmin || !ws.state.IsLocked("file_transfer"), nil
}
func (ws *WebSocketHandler) FileTransferPath(filename string) string {
return ws.state.FileTransferPath(filename)
}
func (ws *WebSocketHandler) FileTransferEnabled() bool {
return ws.conf.FileTransferEnabled
}