diff options
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | hotline/file_path.go | 9 | ||||
| -rw-r--r-- | hotline/file_transfer.go | 2 | ||||
| -rw-r--r-- | internal/mobius/friendship_quest_file_extensions.go | 143 | ||||
| -rw-r--r-- | internal/mobius/transaction_handlers.go | 31 |
5 files changed, 179 insertions, 12 deletions
@@ -5,6 +5,12 @@ </picture> --> +# Mobius (Friendship Quest Remix) + +A fork of [mobius](https://github.com/jhalter/mobius) with some extra features: + +1. If you have upload permission, you get your own `~` folder. + # Mobius Mobius is a cross-platform command line [Hotline](https://en.wikipedia.org/wiki/Hotline_Communications) server implemented in Golang. diff --git a/hotline/file_path.go b/hotline/file_path.go index a3a13f1..aba3195 100644 --- a/hotline/file_path.go +++ b/hotline/file_path.go @@ -98,6 +98,15 @@ func (fp *FilePath) IsUploadDir() bool { return strings.Contains(strings.ToLower(string(fp.Items[fp.Len()-1].Name)), "upload") } +func (fp *FilePath) IsUserDir() bool { + if fp.Len() == 0 { + return false + } + + return strings.HasPrefix(string(fp.Items[0].Name), "~") +} + + func (fp *FilePath) Len() uint16 { return binary.BigEndian.Uint16(fp.ItemCount[:]) } diff --git a/hotline/file_transfer.go b/hotline/file_transfer.go index e8acbb3..386b24a 100644 --- a/hotline/file_transfer.go +++ b/hotline/file_transfer.go @@ -319,7 +319,7 @@ func UploadHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileTransfe // handler should have returned an error to the client indicating there was an existing file present. _, err := os.Stat(fullPath) if err == nil { - return fmt.Errorf("existing file found: %s", fullPath) + // return fmt.Errorf("existing file found: %s", fullPath) } if !errors.Is(err, fs.ErrNotExist) { diff --git a/internal/mobius/friendship_quest_file_extensions.go b/internal/mobius/friendship_quest_file_extensions.go new file mode 100644 index 0000000..c2d5d12 --- /dev/null +++ b/internal/mobius/friendship_quest_file_extensions.go @@ -0,0 +1,143 @@ +package mobius + +import ( + "bytes" + "encoding/binary" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/jhalter/mobius/hotline" +) + +// ResolveUserPath processes paths for the special `~` directory. +func ResolveUserPath(cc *hotline.ClientConn, requestedPath string) (string, error) { + if !strings.HasPrefix(requestedPath, "~") { + return requestedPath, nil + } + + userFolder := filepath.Join("~", cc.Account.Login) + actualUserFolder := filepath.Join(cc.FileRoot(), userFolder) + if stat, err := os.Stat(actualUserFolder); err == nil && stat.IsDir() { + return strings.Replace(requestedPath, "~", userFolder, 1), nil + } + + return "", fmt.Errorf("user folder does not exist") +} + +// updateTransactionPath updates the FieldFilePath in a transaction in-place. +func updateTransactionPath(t *hotline.Transaction, newPath string) { + for i, field := range t.Fields { + if field.Type == hotline.FieldFilePath { + if encodedPath, err := txtEncoder.String(newPath); err == nil { + if fpBytes, err := encodeFilePath(encodedPath); err == nil { + t.Fields[i].Data = fpBytes + } + } + return + } + } +} + +// encodeFilePath encodes a string path into hotline.FilePath binary format. +func encodeFilePath(path string) ([]byte, error) { + components := strings.Split(path, string(filepath.Separator)) + var fp hotline.FilePath + binary.BigEndian.PutUint16(fp.ItemCount[:], uint16(len(components))) + + var buffer bytes.Buffer + if err := binary.Write(&buffer, binary.BigEndian, fp.ItemCount); err != nil { + return nil, err + } + + for _, component := range components { + if component == "" || len(component) > 255 { + return nil, fmt.Errorf("invalid file path component") + } + encodedComponent, err := txtEncoder.String(component) + if err != nil { + return nil, err + } + buffer.Write([]byte{0x00, 0x00, byte(len(encodedComponent))}) + buffer.Write([]byte(encodedComponent)) + } + + return buffer.Bytes(), nil +} + +// handleFileTransaction handles various file operations that require resolving `~` +func handleFileTransaction(cc *hotline.ClientConn, t *hotline.Transaction, handler func(*hotline.ClientConn, *hotline.Transaction) []hotline.Transaction, errMsg string) []hotline.Transaction { + requestedPath, err := hotline.ReadPath(cc.FileRoot(), t.GetField(hotline.FieldFilePath).Data, nil) + if err != nil { + return nil + } + + sliceLen := min(len(cc.FileRoot()) + 1, len(requestedPath)) + resolvedPath, err := ResolveUserPath(cc, requestedPath[sliceLen:]) + if err != nil { + return cc.NewErrReply(t, errMsg) + } + + updateTransactionPath(t, resolvedPath) + return handler(cc, t) +} + +// File operation handlers +func HandleUploadFileWithUserFolders(cc *hotline.ClientConn, t *hotline.Transaction) []hotline.Transaction { + return handleFileTransaction(cc, t, HandleUploadFile, "Cannot upload to non-existent user folder.") +} + +func HandleUploadFolderWithUserFolders(cc *hotline.ClientConn, t *hotline.Transaction) []hotline.Transaction { + return handleFileTransaction(cc, t, HandleUploadFolder, "Cannot upload to non-existent user folder.") +} + +func HandleDeleteFileWithUserFolders(cc *hotline.ClientConn, t *hotline.Transaction) []hotline.Transaction { + return handleFileTransaction(cc, t, HandleDeleteFile, "Cannot delete non-existent file.") +} + +func HandleDownloadFileWithUserFolders(cc *hotline.ClientConn, t *hotline.Transaction) []hotline.Transaction { + return handleFileTransaction(cc, t, HandleDownloadFile, "Cannot download non-existent user file.") +} + +func HandleDownloadFolderWithUserFolders(cc *hotline.ClientConn, t *hotline.Transaction) []hotline.Transaction { + return handleFileTransaction(cc, t, HandleDownloadFolder, "Cannot download non-existent user folder.") +} + +// HandleGetFileNameListWithUserFolders modifies the file listing behavior for `~` +func HandleGetFileNameListWithUserFolders(cc *hotline.ClientConn, t *hotline.Transaction) []hotline.Transaction { + requestedPath, err := hotline.ReadPath(cc.FileRoot(), t.GetField(hotline.FieldFilePath).Data, nil) + if err != nil { + return nil + } + + if requestedPath == cc.FileRoot() { + fileNames, err := hotline.GetFileNameList(cc.FileRoot(), cc.Server.Config.IgnoreFiles) + if err != nil { + return nil + } + + userFolder := filepath.Join(cc.FileRoot(), "~", cc.Account.Login) + if stat, err := os.Stat(userFolder); err != nil || !stat.IsDir() { + filteredFiles := make([]hotline.Field, 0, len(fileNames)) + for _, file := range fileNames { + if !strings.Contains(string(file.Data), "~") { + filteredFiles = append(filteredFiles, file) + } + } + return []hotline.Transaction{cc.NewReply(t, filteredFiles...)} + } + return []hotline.Transaction{cc.NewReply(t, fileNames...)} + } + + if strings.HasPrefix(requestedPath, filepath.Join(cc.FileRoot(), "~")) { + resolvedPath, err := ResolveUserPath(cc, requestedPath[len(cc.FileRoot())+1:]) + if err != nil { + return nil + } + updateTransactionPath(t, resolvedPath) + return HandleGetFileNameList(cc, t) + } + + return HandleGetFileNameList(cc, t) +} diff --git a/internal/mobius/transaction_handlers.go b/internal/mobius/transaction_handlers.go index 20a315d..9916b6c 100644 --- a/internal/mobius/transaction_handlers.go +++ b/internal/mobius/transaction_handlers.go @@ -102,14 +102,14 @@ func RegisterHandlers(srv *hotline.Server) { srv.HandleFunc(hotline.TranChatSend, HandleChatSend) srv.HandleFunc(hotline.TranDelNewsArt, HandleDelNewsArt) srv.HandleFunc(hotline.TranDelNewsItem, HandleDelNewsItem) - srv.HandleFunc(hotline.TranDeleteFile, HandleDeleteFile) + srv.HandleFunc(hotline.TranDeleteFile, HandleDeleteFileWithUserFolders) srv.HandleFunc(hotline.TranDeleteUser, HandleDeleteUser) srv.HandleFunc(hotline.TranDisconnectUser, HandleDisconnectUser) - srv.HandleFunc(hotline.TranDownloadFile, HandleDownloadFile) - srv.HandleFunc(hotline.TranDownloadFldr, HandleDownloadFolder) + srv.HandleFunc(hotline.TranDownloadFile, HandleDownloadFileWithUserFolders) + srv.HandleFunc(hotline.TranDownloadFldr, HandleDownloadFolderWithUserFolders) srv.HandleFunc(hotline.TranGetClientInfoText, HandleGetClientInfoText) srv.HandleFunc(hotline.TranGetFileInfo, HandleGetFileInfo) - srv.HandleFunc(hotline.TranGetFileNameList, HandleGetFileNameList) + srv.HandleFunc(hotline.TranGetFileNameList, HandleGetFileNameListWithUserFolders) srv.HandleFunc(hotline.TranGetMsgs, HandleGetMsgs) srv.HandleFunc(hotline.TranGetNewsArtData, HandleGetNewsArtData) srv.HandleFunc(hotline.TranGetNewsArtNameList, HandleGetNewsArtNameList) @@ -137,8 +137,8 @@ func RegisterHandlers(srv *hotline.Server) { srv.HandleFunc(hotline.TranSetClientUserInfo, HandleSetClientUserInfo) srv.HandleFunc(hotline.TranSetFileInfo, HandleSetFileInfo) srv.HandleFunc(hotline.TranSetUser, HandleSetUser) - srv.HandleFunc(hotline.TranUploadFile, HandleUploadFile) - srv.HandleFunc(hotline.TranUploadFldr, HandleUploadFolder) + srv.HandleFunc(hotline.TranUploadFile, HandleUploadFileWithUserFolders) + srv.HandleFunc(hotline.TranUploadFldr, HandleUploadFolderWithUserFolders) srv.HandleFunc(hotline.TranUserBroadcast, HandleUserBroadcast) srv.HandleFunc(hotline.TranDownloadBanner, HandleDownloadBanner) } @@ -443,6 +443,13 @@ func HandleDeleteFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hot fileName := t.GetField(hotline.FieldFileName).Data filePath := t.GetField(hotline.FieldFilePath).Data + var fp hotline.FilePath + if filePath != nil { + if _, err := fp.Write(filePath); err != nil { + return res + } + } + fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName) if err != nil { return res @@ -460,11 +467,11 @@ func HandleDeleteFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hot switch mode := fi.Mode(); { case mode.IsDir(): - if !cc.Authorize(hotline.AccessDeleteFolder) { + if !cc.Authorize(hotline.AccessDeleteFolder) && !fp.IsUserDir() { return cc.NewErrReply(t, ErrMsgNotAllowedDeleteFolders) } case mode.IsRegular(): - if !cc.Authorize(hotline.AccessDeleteFile) { + if !cc.Authorize(hotline.AccessDeleteFile) && !fp.IsUserDir() { return cc.NewErrReply(t, ErrMsgNotAllowedDeleteFiles) } } @@ -1673,7 +1680,7 @@ func HandleUploadFolder(cc *hotline.ClientConn, t *hotline.Transaction) (res []h // Handle special cases for Upload and Drop Box folders if !cc.Authorize(hotline.AccessUploadAnywhere) { - if !fp.IsUploadDir() && !fp.IsDropbox() { + if !fp.IsUploadDir() && !fp.IsDropbox() && !fp.IsUserDir() { return cc.NewErrReply(t, fmt.Sprintf(ErrMsgUploadRestrictedTemplate, "folder", string(t.GetField(hotline.FieldFileName).Data))) } } @@ -1720,7 +1727,7 @@ func HandleUploadFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hot // Handle special cases for Upload and Drop Box folders if !cc.Authorize(hotline.AccessUploadAnywhere) { - if !fp.IsUploadDir() && !fp.IsDropbox() { + if !fp.IsUploadDir() && !fp.IsDropbox() && !fp.IsUserDir() { return cc.NewErrReply(t, fmt.Sprintf(ErrMsgUploadRestrictedTemplate, "file", string(fileName))) } } @@ -1730,7 +1737,9 @@ func HandleUploadFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hot } if _, err := cc.Server.FS.Stat(fullFilePath); err == nil { - return cc.NewErrReply(t, fmt.Sprintf(ErrMsgFileUploadConflictTemplate, string(fileName))) + if !fp.IsUserDir() { + return cc.NewErrReply(t, fmt.Sprintf(ErrMsgFileUploadConflictTemplate, string(fileName))) + } } ft := cc.NewFileTransfer(hotline.FileUpload, cc.FileRoot(), fileName, filePath, transferSize) |