aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md6
-rw-r--r--hotline/file_path.go9
-rw-r--r--hotline/file_transfer.go2
-rw-r--r--internal/mobius/friendship_quest_file_extensions.go143
-rw-r--r--internal/mobius/transaction_handlers.go31
5 files changed, 179 insertions, 12 deletions
diff --git a/README.md b/README.md
index 3165535..4a988ab 100644
--- a/README.md
+++ b/README.md
@@ -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)