From: Jeff Halter Date: Thu, 23 Jun 2022 21:24:50 +0000 (-0700) Subject: Implement GetClientInfoText with per-client file transfer info X-Git-Url: https://git.r.bdr.sh/rbdr/mobius/commitdiff_plain/df1ade5433b027f9cb905e584921692313e647f5?hp=--cc Implement GetClientInfoText with per-client file transfer info --- df1ade5433b027f9cb905e584921692313e647f5 diff --git a/hotline/client_conn.go b/hotline/client_conn.go index 5780f6d..6a27977 100644 --- a/hotline/client_conn.go +++ b/hotline/client_conn.go @@ -2,11 +2,14 @@ package hotline import ( "encoding/binary" + "fmt" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" "io" "math/big" "sort" + "strings" + "sync" ) type byClientID []*ClientConn @@ -23,33 +26,6 @@ func (s byClientID) Less(i, j int) bool { return s[i].uint16ID() < s[j].uint16ID() } -const template = `Nickname: %s -Name: %s -Account: %s -Address: %s - --------- File Downloads --------- - -%s - -------- Folder Downloads -------- - -None. - ---------- File Uploads ---------- - -None. - --------- Folder Uploads --------- - -None. - -------- Waiting Downloads ------- - -None. - - ` - // ClientConn represents a client connected to a Server type ClientConn struct { Connection io.ReadWriteCloser @@ -64,9 +40,12 @@ type ClientConn struct { Version *[]byte Idle bool AutoReply []byte - Transfers map[int][]*FileTransfer - Agreed bool - logger *zap.SugaredLogger + + transfersMU sync.Mutex + transfers map[int]map[[4]byte]*FileTransfer + + Agreed bool + logger *zap.SugaredLogger } func (cc *ClientConn) sendAll(t int, fields ...Field) { @@ -227,3 +206,56 @@ func sortedClients(unsortedClients map[uint16]*ClientConn) (clients []*ClientCon sort.Sort(byClientID(clients)) return clients } + +const userInfoTemplate = `Nickname: %s +Name: %s +Account: %s +Address: %s + +-------- File Downloads --------- + +%s +------- Folder Downloads -------- + +%s +--------- File Uploads ---------- + +%s +-------- Folder Uploads --------- + +%s +------- Waiting Downloads ------- + +%s +` + +func formatDownloadList(fts map[[4]byte]*FileTransfer) (s string) { + if len(fts) == 0 { + return "None.\n" + } + + for _, dl := range fts { + s += dl.String() + } + + return s +} + +func (cc *ClientConn) String() string { + cc.transfersMU.Lock() + defer cc.transfersMU.Unlock() + template := fmt.Sprintf( + userInfoTemplate, + cc.UserName, + cc.Account.Name, + cc.Account.Login, + cc.RemoteAddr, + formatDownloadList(cc.transfers[FileDownload]), + formatDownloadList(cc.transfers[FolderDownload]), + formatDownloadList(cc.transfers[FileUpload]), + formatDownloadList(cc.transfers[FolderUpload]), + "None.\n", + ) + + return strings.Replace(template, "\n", "\r", -1) +} diff --git a/hotline/file_transfer.go b/hotline/file_transfer.go index c7c1a62..ba4a1e1 100644 --- a/hotline/file_transfer.go +++ b/hotline/file_transfer.go @@ -3,7 +3,10 @@ package hotline import ( "encoding/binary" "fmt" + "math" + "math/rand" "path/filepath" + "sync" ) // File transfer types @@ -16,23 +19,86 @@ const ( ) type FileTransfer struct { - FileName []byte - FilePath []byte - ReferenceNumber []byte - Type int - TransferSize []byte // total size of all items in the folder. Only used in FolderUpload action - FolderItemCount []byte - BytesSent int - clientID uint16 - fileResumeData *FileResumeData - options []byte + FileName []byte + FilePath []byte + ReferenceNumber []byte + refNum [4]byte + Type int + TransferSize []byte + FolderItemCount []byte + fileResumeData *FileResumeData + options []byte + bytesSentCounter *WriteCounter + ClientConn *ClientConn } +// WriteCounter counts the number of bytes written to it. +type WriteCounter struct { + mux sync.Mutex + Total int64 // Total # of bytes written +} + +// Write implements the io.Writer interface. +// +// Always completes and never returns an error. +func (wc *WriteCounter) Write(p []byte) (int, error) { + wc.mux.Lock() + defer wc.mux.Unlock() + n := len(p) + wc.Total += int64(n) + return n, nil +} + +func (cc *ClientConn) newFileTransfer(transferType int, fileName, filePath, size []byte) *FileTransfer { + var transactionRef [4]byte + rand.Read(transactionRef[:]) + + ft := &FileTransfer{ + FileName: fileName, + FilePath: filePath, + ReferenceNumber: transactionRef[:], + refNum: transactionRef, + Type: transferType, + TransferSize: size, + ClientConn: cc, + bytesSentCounter: &WriteCounter{}, + } + + cc.transfersMU.Lock() + defer cc.transfersMU.Unlock() + cc.transfers[transferType][transactionRef] = ft + + cc.Server.mux.Lock() + defer cc.Server.mux.Unlock() + cc.Server.fileTransfers[transactionRef] = ft + + return ft +} + +// String returns a string representation of a file transfer and its progress for display in the GetInfo window +// Example: +// MasterOfOrionII1.4.0. 0% 197.9M func (ft *FileTransfer) String() string { - percentComplete := 10 - out := fmt.Sprintf("%s\t %v", ft.FileName, percentComplete) + trunc := fmt.Sprintf("%.21s", ft.FileName) + return fmt.Sprintf("%-21s %.3s%% %6s\n", trunc, ft.percentComplete(), ft.formattedTransferSize()) +} - return out +func (ft *FileTransfer) percentComplete() string { + ft.bytesSentCounter.mux.Lock() + defer ft.bytesSentCounter.mux.Unlock() + return fmt.Sprintf( + "%v", + math.RoundToEven(float64(ft.bytesSentCounter.Total)/float64(binary.BigEndian.Uint32(ft.TransferSize))*100), + ) +} + +func (ft *FileTransfer) formattedTransferSize() string { + sizeInKB := float32(binary.BigEndian.Uint32(ft.TransferSize)) / 1024 + if sizeInKB > 1024 { + return fmt.Sprintf("%.1fM", sizeInKB/1024) + } else { + return fmt.Sprintf("%.0fK", sizeInKB) + } } func (ft *FileTransfer) ItemCount() int { diff --git a/hotline/file_wrapper.go b/hotline/file_wrapper.go index b59fc5c..f3fd559 100644 --- a/hotline/file_wrapper.go +++ b/hotline/file_wrapper.go @@ -105,7 +105,7 @@ func (f *fileWrapper) infoForkName() string { return fmt.Sprintf(infoForkNameTemplate, f.name) } -func (f *fileWrapper) rsrcForkWriter() (io.Writer, error) { +func (f *fileWrapper) rsrcForkWriter() (io.WriteCloser, error) { file, err := os.OpenFile(f.rsrcPath, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return nil, err @@ -114,7 +114,7 @@ func (f *fileWrapper) rsrcForkWriter() (io.Writer, error) { return file, nil } -func (f *fileWrapper) infoForkWriter() (io.Writer, error) { +func (f *fileWrapper) infoForkWriter() (io.WriteCloser, error) { file, err := os.OpenFile(f.infoPath, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return nil, err @@ -123,7 +123,7 @@ func (f *fileWrapper) infoForkWriter() (io.Writer, error) { return file, nil } -func (f *fileWrapper) incFileWriter() (io.Writer, error) { +func (f *fileWrapper) incFileWriter() (io.WriteCloser, error) { file, err := os.OpenFile(f.incompletePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil { return nil, err diff --git a/hotline/server.go b/hotline/server.go index 21b1316..e685afd 100644 --- a/hotline/server.go +++ b/hotline/server.go @@ -43,12 +43,14 @@ const ( var nostalgiaVersion = []byte{0, 0, 2, 0x2c} // version ID used by the Nostalgia client type Server struct { - Port int - Accounts map[string]*Account - Agreement []byte - Clients map[uint16]*ClientConn - ThreadedNews *ThreadedNews - FileTransfers map[uint32]*FileTransfer + Port int + Accounts map[string]*Account + Agreement []byte + Clients map[uint16]*ClientConn + ThreadedNews *ThreadedNews + + fileTransfers map[[4]byte]*FileTransfer + Config *Config ConfigDir string Logger *zap.SugaredLogger @@ -214,7 +216,7 @@ func NewServer(configDir string, netPort int, logger *zap.SugaredLogger, FS File Accounts: make(map[string]*Account), Config: new(Config), Clients: make(map[uint16]*ClientConn), - FileTransfers: make(map[uint32]*FileTransfer), + fileTransfers: make(map[[4]byte]*FileTransfer), PrivateChats: make(map[uint32]*PrivateChat), ConfigDir: configDir, Logger: logger, @@ -354,10 +356,18 @@ func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *Clie Server: s, Version: &[]byte{}, AutoReply: []byte{}, - Transfers: make(map[int][]*FileTransfer), + transfers: map[int]map[[4]byte]*FileTransfer{}, Agreed: false, RemoteAddr: remoteAddr, } + clientConn.transfers = map[int]map[[4]byte]*FileTransfer{ + FileDownload: {}, + FileUpload: {}, + FolderDownload: {}, + FolderUpload: {}, + bannerDownload: {}, + } + *s.NextGuestID++ ID := *s.NextGuestID @@ -651,16 +661,6 @@ func (s *Server) handleNewConnection(ctx context.Context, conn io.ReadWriteClose } } -// NewTransactionRef generates a random ID for the file transfer. The Hotline client includes this ID -// in the transfer request payload, and the file transfer server will use it to map the request -// to a transfer -func (s *Server) NewTransactionRef() []byte { - transactionRef := make([]byte, 4) - rand.Read(transactionRef) - - return transactionRef -} - func (s *Server) NewPrivateChat(cc *ClientConn) []byte { s.mux.Lock() defer s.mux.Unlock() @@ -696,56 +696,62 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro return err } - transferRefNum := binary.BigEndian.Uint32(t.ReferenceNumber[:]) defer func() { s.mux.Lock() - delete(s.FileTransfers, transferRefNum) + delete(s.fileTransfers, t.ReferenceNumber) s.mux.Unlock() + }() s.mux.Lock() - fileTransfer, ok := s.FileTransfers[transferRefNum] + fileTransfer, ok := s.fileTransfers[t.ReferenceNumber] s.mux.Unlock() if !ok { return errors.New("invalid transaction ID") } + defer func() { + fileTransfer.ClientConn.transfersMU.Lock() + delete(fileTransfer.ClientConn.transfers[fileTransfer.Type], t.ReferenceNumber) + fileTransfer.ClientConn.transfersMU.Unlock() + }() + rLogger := s.Logger.With( "remoteAddr", ctx.Value(contextKeyReq).(requestCtx).remoteAddr, - "xferID", transferRefNum, + "login", fileTransfer.ClientConn.Account.Login, + "name", string(fileTransfer.ClientConn.UserName), ) + fullPath, err := readPath(s.Config.FileRoot, fileTransfer.FilePath, fileTransfer.FileName) + if err != nil { + return err + } + switch fileTransfer.Type { case bannerDownload: if err := s.bannerDownload(rwc); err != nil { + panic(err) return err } case FileDownload: s.Stats.DownloadCounter += 1 - fullFilePath, err := readPath(s.Config.FileRoot, fileTransfer.FilePath, fileTransfer.FileName) - if err != nil { - return err - } - var dataOffset int64 if fileTransfer.fileResumeData != nil { dataOffset = int64(binary.BigEndian.Uint32(fileTransfer.fileResumeData.ForkInfoList[0].DataSize[:])) } - fw, err := newFileWrapper(s.FS, fullFilePath, 0) + fw, err := newFileWrapper(s.FS, fullPath, 0) if err != nil { return err } - rLogger.Infow("File download started", "filePath", fullFilePath, "transactionRef", fileTransfer.ReferenceNumber) - - wr := bufio.NewWriterSize(rwc, 1460) + rLogger.Infow("File download started", "filePath", fullPath) // if file transfer options are included, that means this is a "quick preview" request from a 1.5+ client if fileTransfer.options == nil { // Start by sending flat file object to client - if _, err := wr.Write(fw.ffo.BinaryMarshal()); err != nil { + if _, err := rwc.Write(fw.ffo.BinaryMarshal()); err != nil { return err } } @@ -755,23 +761,21 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro return err } - if err := sendFile(wr, file, int(dataOffset)); err != nil { + br := bufio.NewReader(file) + if _, err := br.Discard(int(dataOffset)); err != nil { return err } - if err := wr.Flush(); err != nil { + if _, err = io.Copy(rwc, io.TeeReader(br, fileTransfer.bytesSentCounter)); err != nil { return err } - // if the client requested to resume transfer, do not send the resource fork, or it will be appended into the fileWrapper data + // if the client requested to resume transfer, do not send the resource fork header, or it will be appended into the fileWrapper data if fileTransfer.fileResumeData == nil { - err = binary.Write(wr, binary.BigEndian, fw.rsrcForkHeader()) + err = binary.Write(rwc, binary.BigEndian, fw.rsrcForkHeader()) if err != nil { return err } - if err := wr.Flush(); err != nil { - return err - } } rFile, err := fw.rsrcForkFile() @@ -779,23 +783,13 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro return nil } - err = sendFile(wr, rFile, int(dataOffset)) - if err != nil { - return err - } - - if err := wr.Flush(); err != nil { + if _, err = io.Copy(rwc, io.TeeReader(rFile, fileTransfer.bytesSentCounter)); err != nil { return err } case FileUpload: s.Stats.UploadCounter += 1 - destinationFile, err := readPath(s.Config.FileRoot, fileTransfer.FilePath, fileTransfer.FileName) - if err != nil { - return err - } - var file *os.File // A file upload has three possible cases: @@ -805,28 +799,24 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro // We have to infer which case applies by inspecting what is already on the filesystem // 1) Check for existing file: - _, err = os.Stat(destinationFile) + _, err = os.Stat(fullPath) if err == nil { - // If found, that means this upload is intended to replace the file - if err = os.Remove(destinationFile); err != nil { - return err - } - file, err = os.Create(destinationFile + incompleteFileSuffix) + return errors.New("existing file found at " + fullPath) } if errors.Is(err, fs.ErrNotExist) { // If not found, open or create a new .incomplete file - file, err = os.OpenFile(destinationFile+incompleteFileSuffix, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + file, err = os.OpenFile(fullPath+incompleteFileSuffix, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil { return err } } - f, err := newFileWrapper(s.FS, destinationFile, 0) + f, err := newFileWrapper(s.FS, fullPath, 0) if err != nil { return err } - s.Logger.Infow("File upload started", "transactionRef", fileTransfer.ReferenceNumber, "dstFile", destinationFile) + rLogger.Infow("File upload started", "dstFile", fullPath) rForkWriter := io.Discard iForkWriter := io.Discard @@ -842,19 +832,19 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro } } - if err := receiveFile(rwc, file, rForkWriter, iForkWriter); err != nil { - return err + if err := receiveFile(rwc, file, rForkWriter, iForkWriter, fileTransfer.bytesSentCounter); err != nil { + s.Logger.Error(err) } if err := file.Close(); err != nil { return err } - if err := s.FS.Rename(destinationFile+".incomplete", destinationFile); err != nil { + if err := s.FS.Rename(fullPath+".incomplete", fullPath); err != nil { return err } - s.Logger.Infow("File upload complete", "transactionRef", fileTransfer.ReferenceNumber, "dstFile", destinationFile) + rLogger.Infow("File upload complete", "dstFile", fullPath) case FolderDownload: // Folder Download flow: // 1. Get filePath from the transfer @@ -883,14 +873,9 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro // // This notifies the server to send the next item header - fullFilePath, err := readPath(s.Config.FileRoot, fileTransfer.FilePath, fileTransfer.FileName) - if err != nil { - return err - } + basePathLen := len(fullPath) - basePathLen := len(fullFilePath) - - s.Logger.Infow("Start folder download", "path", fullFilePath, "ReferenceNumber", fileTransfer.ReferenceNumber) + rLogger.Infow("Start folder download", "path", fullPath) nextAction := make([]byte, 2) if _, err := io.ReadFull(rwc, nextAction); err != nil { @@ -898,7 +883,7 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro } i := 0 - err = filepath.Walk(fullFilePath+"/", func(path string, info os.FileInfo, err error) error { + err = filepath.Walk(fullPath+"/", func(path string, info os.FileInfo, err error) error { s.Stats.DownloadCounter += 1 i += 1 @@ -917,7 +902,7 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro } subPath := path[basePathLen+1:] - s.Logger.Infow("Sending fileheader", "i", i, "path", path, "fullFilePath", fullFilePath, "subPath", subPath, "IsDir", info.IsDir()) + rLogger.Debugw("Sending fileheader", "i", i, "path", path, "fullFilePath", fullPath, "subPath", subPath, "IsDir", info.IsDir()) if i == 1 { return nil @@ -936,7 +921,7 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro return err } - s.Logger.Infow("Client folder download action", "action", fmt.Sprintf("%X", nextAction[0:2])) + rLogger.Debugw("Client folder download action", "action", fmt.Sprintf("%X", nextAction[0:2])) var dataOffset int64 @@ -968,9 +953,8 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro return nil } - s.Logger.Infow("File download started", + rLogger.Infow("File download started", "fileName", info.Name(), - "transactionRef", fileTransfer.ReferenceNumber, "TransferSize", fmt.Sprintf("%x", hlFile.ffo.TransferSize(dataOffset)), ) @@ -992,8 +976,7 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro } // wr := bufio.NewWriterSize(rwc, 1460) - err = sendFile(rwc, file, int(dataOffset)) - if err != nil { + if _, err = io.Copy(rwc, io.TeeReader(file, fileTransfer.bytesSentCounter)); err != nil { return err } @@ -1008,8 +991,7 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro return err } - err = sendFile(rwc, rFile, int(dataOffset)) - if err != nil { + if _, err = io.Copy(rwc, io.TeeReader(rFile, fileTransfer.bytesSentCounter)); err != nil { return err } } @@ -1027,22 +1009,16 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro } case FolderUpload: - dstPath, err := readPath(s.Config.FileRoot, fileTransfer.FilePath, fileTransfer.FileName) - if err != nil { - return err - } - - s.Logger.Infow( + rLogger.Infow( "Folder upload started", - "transactionRef", fileTransfer.ReferenceNumber, - "dstPath", dstPath, - "TransferSize", fmt.Sprintf("%x", fileTransfer.TransferSize), + "dstPath", fullPath, + "TransferSize", binary.BigEndian.Uint32(fileTransfer.TransferSize), "FolderItemCount", fileTransfer.FolderItemCount, ) // Check if the target folder exists. If not, create it. - if _, err := s.FS.Stat(dstPath); os.IsNotExist(err) { - if err := s.FS.Mkdir(dstPath, 0777); err != nil { + if _, err := s.FS.Stat(fullPath); os.IsNotExist(err) { + if err := s.FS.Mkdir(fullPath, 0777); err != nil { return err } } @@ -1074,17 +1050,16 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro return err } - s.Logger.Infow( + rLogger.Infow( "Folder upload continued", - "transactionRef", fmt.Sprintf("%x", fileTransfer.ReferenceNumber), "FormattedPath", fu.FormattedPath(), "IsFolder", fmt.Sprintf("%x", fu.IsFolder), "PathItemCount", binary.BigEndian.Uint16(fu.PathItemCount[:]), ) if fu.IsFolder == [2]byte{0, 1} { - if _, err := os.Stat(filepath.Join(dstPath, fu.FormattedPath())); os.IsNotExist(err) { - if err := os.Mkdir(filepath.Join(dstPath, fu.FormattedPath()), 0777); err != nil { + if _, err := os.Stat(filepath.Join(fullPath, fu.FormattedPath())); os.IsNotExist(err) { + if err := os.Mkdir(filepath.Join(fullPath, fu.FormattedPath()), 0777); err != nil { return err } } @@ -1097,7 +1072,7 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro nextAction := dlFldrActionSendFile // Check if we have the full file already. If so, send dlFldrAction_NextFile to client to skip. - _, err = os.Stat(filepath.Join(dstPath, fu.FormattedPath())) + _, err = os.Stat(filepath.Join(fullPath, fu.FormattedPath())) if err != nil && !errors.Is(err, fs.ErrNotExist) { return err } @@ -1106,7 +1081,7 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro } // Check if we have a partial file already. If so, send dlFldrAction_ResumeFile to client to resume upload. - incompleteFile, err := os.Stat(filepath.Join(dstPath, fu.FormattedPath()+incompleteFileSuffix)) + incompleteFile, err := os.Stat(filepath.Join(fullPath, fu.FormattedPath()+incompleteFileSuffix)) if err != nil && !errors.Is(err, fs.ErrNotExist) { return err } @@ -1125,7 +1100,7 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro offset := make([]byte, 4) binary.BigEndian.PutUint32(offset, uint32(incompleteFile.Size())) - file, err := os.OpenFile(dstPath+"/"+fu.FormattedPath()+incompleteFileSuffix, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + file, err := os.OpenFile(fullPath+"/"+fu.FormattedPath()+incompleteFileSuffix, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return err } @@ -1145,11 +1120,11 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro return err } - if err := receiveFile(rwc, file, ioutil.Discard, ioutil.Discard); err != nil { + if err := receiveFile(rwc, file, ioutil.Discard, ioutil.Discard, fileTransfer.bytesSentCounter); err != nil { s.Logger.Error(err) } - err = os.Rename(dstPath+"/"+fu.FormattedPath()+".incomplete", dstPath+"/"+fu.FormattedPath()) + err = os.Rename(fullPath+"/"+fu.FormattedPath()+".incomplete", fullPath+"/"+fu.FormattedPath()) if err != nil { return err } @@ -1159,14 +1134,14 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro return err } - filePath := filepath.Join(dstPath, fu.FormattedPath()) + filePath := filepath.Join(fullPath, fu.FormattedPath()) hlFile, err := newFileWrapper(s.FS, filePath, 0) if err != nil { return err } - s.Logger.Infow("Starting file transfer", "path", filePath, "fileNum", i+1, "fileSize", binary.BigEndian.Uint32(fileSize)) + rLogger.Infow("Starting file transfer", "path", filePath, "fileNum", i+1, "fileSize", binary.BigEndian.Uint32(fileSize)) incWriter, err := hlFile.incFileWriter() if err != nil { @@ -1186,10 +1161,10 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro return err } } - if err := receiveFile(rwc, incWriter, rForkWriter, iForkWriter); err != nil { + if err := receiveFile(rwc, incWriter, rForkWriter, iForkWriter, fileTransfer.bytesSentCounter); err != nil { return err } - // _ = newFile.Close() + if err := os.Rename(filePath+".incomplete", filePath); err != nil { return err } @@ -1201,7 +1176,7 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro } } } - s.Logger.Infof("Folder upload complete") + rLogger.Infof("Folder upload complete") } return nil diff --git a/hotline/server_test.go b/hotline/server_test.go index d3e4325..bf608e1 100644 --- a/hotline/server_test.go +++ b/hotline/server_test.go @@ -32,7 +32,7 @@ func TestServer_handleFileTransfer(t *testing.T) { Agreement []byte Clients map[uint16]*ClientConn ThreadedNews *ThreadedNews - FileTransfers map[uint32]*FileTransfer + fileTransfers map[[4]byte]*FileTransfer Config *Config ConfigDir string Logger *zap.SugaredLogger @@ -116,12 +116,24 @@ func TestServer_handleFileTransfer(t *testing.T) { }()}, Logger: NewTestLogger(), Stats: &Stats{}, - FileTransfers: map[uint32]*FileTransfer{ - uint32(5): { + fileTransfers: map[[4]byte]*FileTransfer{ + [4]byte{0, 0, 0, 5}: { ReferenceNumber: []byte{0, 0, 0, 5}, Type: FileDownload, FileName: []byte("testfile-8b"), FilePath: []byte{}, + ClientConn: &ClientConn{ + Account: &Account{ + Login: "foo", + }, + transfersMU: sync.Mutex{}, + transfers: map[int]map[[4]byte]*FileTransfer{ + FileDownload: { + [4]byte{0, 0, 0, 5}: &FileTransfer{}, + }, + }, + }, + bytesSentCounter: &WriteCounter{}, }, }, }, @@ -168,7 +180,7 @@ func TestServer_handleFileTransfer(t *testing.T) { Agreement: tt.fields.Agreement, Clients: tt.fields.Clients, ThreadedNews: tt.fields.ThreadedNews, - FileTransfers: tt.fields.FileTransfers, + fileTransfers: tt.fields.fileTransfers, Config: tt.fields.Config, ConfigDir: tt.fields.ConfigDir, Logger: tt.fields.Logger, diff --git a/hotline/transaction_handlers.go b/hotline/transaction_handlers.go index f6fa232..735e155 100644 --- a/hotline/transaction_handlers.go +++ b/hotline/transaction_handlers.go @@ -87,7 +87,7 @@ var TransactionHandlers = map[uint16]TransactionType{ }, tranGetClientInfoText: { Name: "tranGetClientInfoText", - Handler: HandleGetClientConnInfoText, + Handler: HandleGetClientInfoText, }, tranGetFileInfo: { Name: "tranGetFileInfo", @@ -867,9 +867,17 @@ func byteToInt(bytes []byte) (int, error) { return 0, errors.New("unknown byte length") } -func HandleGetClientConnInfoText(cc *ClientConn, t *Transaction) (res []Transaction, err error) { +// HandleGetClientInfoText returns user information for the specific user. +// +// Fields used in the request: +// 103 User ID +// +// Fields used in the reply: +// 102 User name +// 101 Data User info text string +func HandleGetClientInfoText(cc *ClientConn, t *Transaction) (res []Transaction, err error) { if !authorize(cc.Account.Access, accessGetClientInfo) { - res = append(res, cc.NewErrReply(t, "You are not allowed to get client info")) + res = append(res, cc.NewErrReply(t, "You are not allowed to get client info.")) return res, err } @@ -877,55 +885,11 @@ func HandleGetClientConnInfoText(cc *ClientConn, t *Transaction) (res []Transact clientConn := cc.Server.Clients[uint16(clientID)] if clientConn == nil { - return res, errors.New("invalid client") + return append(res, cc.NewErrReply(t, "User not found.")), err } - // TODO: Implement non-hardcoded values - template := `Nickname: %s -Name: %s -Account: %s -Address: %s - --------- File Downloads --------- - -%s - -------- Folder Downloads -------- - -None. - ---------- File Uploads ---------- - -None. - --------- Folder Uploads --------- - -None. - -------- Waiting Downloads ------- - -None. - - ` - - activeDownloads := clientConn.Transfers[FileDownload] - activeDownloadList := "None." - for _, dl := range activeDownloads { - activeDownloadList += dl.String() + "\n" - } - - template = fmt.Sprintf( - template, - clientConn.UserName, - clientConn.Account.Name, - clientConn.Account.Login, - clientConn.RemoteAddr, - activeDownloadList, - ) - template = strings.Replace(template, "\n", "\r", -1) - res = append(res, cc.NewReply(t, - NewField(fieldData, []byte(template)), + NewField(fieldData, []byte(clientConn.String())), NewField(fieldUserName, clientConn.UserName), )) return res, err @@ -1403,15 +1367,9 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err return res, err } - transactionRef := cc.Server.NewTransactionRef() - data := binary.BigEndian.Uint32(transactionRef) + xferSize := hlFile.ffo.TransferSize(0) - ft := &FileTransfer{ - FileName: fileName, - FilePath: filePath, - ReferenceNumber: transactionRef, - Type: FileDownload, - } + ft := cc.newFileTransfer(FileDownload, fileName, filePath, xferSize) // TODO: refactor to remove this if resumeData != nil { @@ -1422,8 +1380,6 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err ft.fileResumeData = &frd } - xferSize := hlFile.ffo.TransferSize(0) - // Optional field for when a HL v1.5+ client requests file preview // Used only for TEXT, JPEG, GIFF, BMP or PICT files // The value will always be 2 @@ -1432,14 +1388,8 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err xferSize = hlFile.ffo.FlatFileDataForkHeader.DataSize[:] } - cc.Server.mux.Lock() - defer cc.Server.mux.Unlock() - cc.Server.FileTransfers[data] = ft - - cc.Transfers[FileDownload] = append(cc.Transfers[FileDownload], ft) - res = append(res, cc.NewReply(t, - NewField(fieldRefNum, transactionRef), + NewField(fieldRefNum, ft.refNum[:]), NewField(fieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count NewField(fieldTransferSize, xferSize), NewField(fieldFileSize, hlFile.ffo.FlatFileDataForkHeader.DataSize[:]), @@ -1455,26 +1405,6 @@ func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction, er return res, err } - transactionRef := cc.Server.NewTransactionRef() - data := binary.BigEndian.Uint32(transactionRef) - - fileTransfer := &FileTransfer{ - FileName: t.GetField(fieldFileName).Data, - FilePath: t.GetField(fieldFilePath).Data, - ReferenceNumber: transactionRef, - Type: FolderDownload, - } - cc.Server.mux.Lock() - cc.Server.FileTransfers[data] = fileTransfer - cc.Server.mux.Unlock() - cc.Transfers[FolderDownload] = append(cc.Transfers[FolderDownload], fileTransfer) - - var fp FilePath - err = fp.UnmarshalBinary(t.GetField(fieldFilePath).Data) - if err != nil { - return res, err - } - fullFilePath, err := readPath(cc.Server.Config.FileRoot, t.GetField(fieldFilePath).Data, t.GetField(fieldFileName).Data) if err != nil { return res, err @@ -1488,8 +1418,17 @@ func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction, er if err != nil { return res, err } + + fileTransfer := cc.newFileTransfer(FolderDownload, t.GetField(fieldFileName).Data, t.GetField(fieldFilePath).Data, transferSize) + + var fp FilePath + err = fp.UnmarshalBinary(t.GetField(fieldFilePath).Data) + if err != nil { + return res, err + } + res = append(res, cc.NewReply(t, - NewField(fieldRefNum, transactionRef), + NewField(fieldRefNum, fileTransfer.ReferenceNumber), NewField(fieldTransferSize, transferSize), NewField(fieldFolderItemCount, itemCount), NewField(fieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count @@ -1505,9 +1444,6 @@ func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction, er // 220 Folder item count // 204 File transfer options "Optional Currently set to 1" (TODO: ??) func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) { - transactionRef := cc.Server.NewTransactionRef() - data := binary.BigEndian.Uint32(transactionRef) - var fp FilePath if t.GetField(fieldFilePath).Data != nil { if err = fp.UnmarshalBinary(t.GetField(fieldFilePath).Data); err != nil { @@ -1523,17 +1459,15 @@ func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err } } - fileTransfer := &FileTransfer{ - FileName: t.GetField(fieldFileName).Data, - FilePath: t.GetField(fieldFilePath).Data, - ReferenceNumber: transactionRef, - Type: FolderUpload, - FolderItemCount: t.GetField(fieldFolderItemCount).Data, - TransferSize: t.GetField(fieldTransferSize).Data, - } - cc.Server.FileTransfers[data] = fileTransfer + fileTransfer := cc.newFileTransfer(FolderUpload, + t.GetField(fieldFileName).Data, + t.GetField(fieldFilePath).Data, + t.GetField(fieldTransferSize).Data, + ) + + fileTransfer.FolderItemCount = t.GetField(fieldFolderItemCount).Data - res = append(res, cc.NewReply(t, NewField(fieldRefNum, transactionRef))) + res = append(res, cc.NewReply(t, NewField(fieldRefNum, fileTransfer.ReferenceNumber))) return res, err } @@ -1552,11 +1486,8 @@ func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err er fileName := t.GetField(fieldFileName).Data filePath := t.GetField(fieldFilePath).Data - transferOptions := t.GetField(fieldFileTransferOptions).Data - - // TODO: is this field useful for anything? - // transferSize := t.GetField(fieldTransferSize).Data + transferSize := t.GetField(fieldTransferSize).Data // not sent for resume var fp FilePath if filePath != nil { @@ -1572,27 +1503,22 @@ func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err er return res, err } } + fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName) + if err != nil { + return res, err + } - transactionRef := cc.Server.NewTransactionRef() - data := binary.BigEndian.Uint32(transactionRef) - - cc.Server.mux.Lock() - cc.Server.FileTransfers[data] = &FileTransfer{ - FileName: fileName, - FilePath: filePath, - ReferenceNumber: transactionRef, - Type: FileUpload, + if _, err := cc.Server.FS.Stat(fullFilePath); err == nil { + res = append(res, cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload because there is already a file named \"%v\". Try choosing a different name.", string(fileName)))) + return res, err } - cc.Server.mux.Unlock() - replyT := cc.NewReply(t, NewField(fieldRefNum, transactionRef)) + ft := cc.newFileTransfer(FileUpload, fileName, filePath, transferSize) + + replyT := cc.NewReply(t, NewField(fieldRefNum, ft.ReferenceNumber)) // client has requested to resume a partially transferred file if transferOptions != nil { - fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName) - if err != nil { - return res, err - } fileInfo, err := cc.Server.FS.Stat(fullFilePath + incompleteFileSuffix) if err != nil { @@ -1608,6 +1534,8 @@ func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err er b, _ := fileResumeData.BinaryMarshal() + ft.TransferSize = offset + replyT.Fields = append(replyT.Fields, NewField(fieldFileResumeData, b)) } @@ -1937,29 +1865,18 @@ func HandleMakeAlias(cc *ClientConn, t *Transaction) (res []Transaction, err err } func HandleDownloadBanner(cc *ClientConn, t *Transaction) (res []Transaction, err error) { - transactionRef := cc.Server.NewTransactionRef() - data := binary.BigEndian.Uint32(transactionRef) - - ft := &FileTransfer{ - ReferenceNumber: transactionRef, - Type: bannerDownload, - } - fi, err := cc.Server.FS.Stat(filepath.Join(cc.Server.ConfigDir, cc.Server.Config.BannerFile)) if err != nil { return res, err } - size := make([]byte, 4) - binary.BigEndian.PutUint32(size, uint32(fi.Size())) + ft := cc.newFileTransfer(bannerDownload, []byte{}, []byte{}, make([]byte, 4)) - cc.Server.mux.Lock() - defer cc.Server.mux.Unlock() - cc.Server.FileTransfers[data] = ft + binary.BigEndian.PutUint32(ft.TransferSize, uint32(fi.Size())) res = append(res, cc.NewReply(t, - NewField(fieldRefNum, transactionRef), - NewField(fieldTransferSize, size), + NewField(fieldRefNum, ft.refNum[:]), + NewField(fieldTransferSize, ft.TransferSize), )) return res, err diff --git a/hotline/transaction_handlers_test.go b/hotline/transaction_handlers_test.go index 424a8e5..e379f09 100644 --- a/hotline/transaction_handlers_test.go +++ b/hotline/transaction_handlers_test.go @@ -978,7 +978,13 @@ func TestHandleUploadFile(t *testing.T) { args: args{ cc: &ClientConn{ Server: &Server{ - FileTransfers: map[uint32]*FileTransfer{}, + FS: &OSFileStore{}, + fileTransfers: map[[4]byte]*FileTransfer{}, + Config: &Config{ + FileRoot: func() string { path, _ := os.Getwd(); return path + "/test/config/Files" }(), + }}, + transfers: map[int]map[[4]byte]*FileTransfer{ + FileUpload: {}, }, Account: &Account{ Access: func() *[]byte { @@ -1026,9 +1032,6 @@ func TestHandleUploadFile(t *testing.T) { return &access }(), }, - Server: &Server{ - FileTransfers: map[uint32]*FileTransfer{}, - }, }, t: NewTransaction( tranUploadFile, &[]byte{0, 1}, @@ -1744,7 +1747,9 @@ func TestHandleDownloadFile(t *testing.T) { name: "with a valid file", args: args{ cc: &ClientConn{ - Transfers: make(map[int][]*FileTransfer), + transfers: map[int]map[[4]byte]*FileTransfer{ + FileDownload: {}, + }, Account: &Account{ Access: func() *[]byte { var bits accessBitmap @@ -1755,7 +1760,7 @@ func TestHandleDownloadFile(t *testing.T) { }, Server: &Server{ FS: &OSFileStore{}, - FileTransfers: make(map[uint32]*FileTransfer), + fileTransfers: map[[4]byte]*FileTransfer{}, Config: &Config{ FileRoot: func() string { path, _ := os.Getwd(); return path + "/test/config/Files" }(), }, @@ -1790,8 +1795,9 @@ func TestHandleDownloadFile(t *testing.T) { name: "when client requests to resume 1k test file at offset 256", args: args{ cc: &ClientConn{ - Transfers: make(map[int][]*FileTransfer), - Account: &Account{ + transfers: map[int]map[[4]byte]*FileTransfer{ + FileDownload: {}, + }, Account: &Account{ Access: func() *[]byte { var bits accessBitmap bits.Set(accessDownloadFile) @@ -1801,6 +1807,7 @@ func TestHandleDownloadFile(t *testing.T) { }, Server: &Server{ FS: &OSFileStore{}, + // FS: func() *MockFileStore { // path, _ := os.Getwd() // testFile, err := os.Open(path + "/test/config/Files/testfile-1k") @@ -1818,7 +1825,7 @@ func TestHandleDownloadFile(t *testing.T) { // // return mfs // }(), - FileTransfers: make(map[uint32]*FileTransfer), + fileTransfers: map[[4]byte]*FileTransfer{}, Config: &Config{ FileRoot: func() string { path, _ := os.Getwd(); return path + "/test/config/Files" }(), }, @@ -2611,3 +2618,149 @@ func TestHandleGetFileNameList(t *testing.T) { }) } } + +func TestHandleGetClientInfoText(t *testing.T) { + type args struct { + cc *ClientConn + t *Transaction + } + tests := []struct { + name string + args args + wantRes []Transaction + wantErr assert.ErrorAssertionFunc + }{ + { + name: "when user does not have required permission", + args: args{ + cc: &ClientConn{ + Account: &Account{ + Access: func() *[]byte { + var bits accessBitmap + access := bits[:] + return &access + }(), + }, + Server: &Server{ + Accounts: map[string]*Account{}, + }, + }, + t: NewTransaction( + tranGetClientInfoText, &[]byte{0, 1}, + NewField(fieldUserID, []byte{0, 1}), + ), + }, + wantRes: []Transaction{ + { + Flags: 0x00, + IsReply: 0x01, + Type: []byte{0, 0x00}, + ID: []byte{0, 0, 0, 0}, + ErrorCode: []byte{0, 0, 0, 1}, + Fields: []Field{ + NewField(fieldError, []byte("You are not allowed to get client info.")), + }, + }, + }, + wantErr: assert.NoError, + }, + { + name: "with a valid user", + args: args{ + cc: &ClientConn{ + UserName: []byte("Testy McTest"), + RemoteAddr: "1.2.3.4:12345", + Account: &Account{ + Access: func() *[]byte { + var bits accessBitmap + bits.Set(accessGetClientInfo) + access := bits[:] + return &access + }(), + Name: "test", + Login: "test", + }, + Server: &Server{ + Accounts: map[string]*Account{}, + Clients: map[uint16]*ClientConn{ + uint16(1): { + UserName: []byte("Testy McTest"), + RemoteAddr: "1.2.3.4:12345", + Account: &Account{ + Access: func() *[]byte { + var bits accessBitmap + bits.Set(accessGetClientInfo) + access := bits[:] + return &access + }(), + Name: "test", + Login: "test", + }, + }, + }, + }, + transfers: map[int]map[[4]byte]*FileTransfer{ + FileDownload: {}, + FileUpload: {}, + FolderDownload: {}, + FolderUpload: {}, + }, + }, + t: NewTransaction( + tranGetClientInfoText, &[]byte{0, 1}, + NewField(fieldUserID, []byte{0, 1}), + ), + }, + wantRes: []Transaction{ + { + Flags: 0x00, + IsReply: 0x01, + Type: []byte{0x1, 0x2f}, + ID: []byte{0, 0, 0, 0}, + ErrorCode: []byte{0, 0, 0, 0}, + Fields: []Field{ + NewField(fieldData, []byte( + strings.Replace(`Nickname: Testy McTest +Name: test +Account: test +Address: 1.2.3.4:12345 + +-------- File Downloads --------- + +None. + +------- Folder Downloads -------- + +None. + +--------- File Uploads ---------- + +None. + +-------- Folder Uploads --------- + +None. + +------- Waiting Downloads ------- + +None. + +`, "\n", "\r", -1)), + ), + NewField(fieldUserName, []byte("Testy McTest")), + }, + }, + }, + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotRes, err := HandleGetClientInfoText(tt.args.cc, tt.args.t) + if !tt.wantErr(t, err, fmt.Sprintf("HandleGetClientInfoText(%v, %v)", tt.args.cc, tt.args.t)) { + return + } + tranAssertEqual(t, tt.wantRes, gotRes) + }) + } +} diff --git a/hotline/transfer.go b/hotline/transfer.go index 9d2a97f..4c80581 100644 --- a/hotline/transfer.go +++ b/hotline/transfer.go @@ -1,7 +1,6 @@ package hotline import ( - "bufio" "bytes" "encoding/binary" "errors" @@ -31,9 +30,7 @@ func (tf *transfer) Write(b []byte) (int, error) { return len(b), nil } -const fileCopyBufSize = 4096 - -func receiveFile(r io.Reader, targetFile, resForkFile, infoFork io.Writer) error { +func receiveFile(r io.Reader, targetFile, resForkFile, infoFork, counterWriter io.Writer) error { var ffo flattenedFileObject if _, err := ffo.ReadFrom(r); err != nil { return err @@ -45,12 +42,7 @@ func receiveFile(r io.Reader, targetFile, resForkFile, infoFork io.Writer) error return err } - // read and write the data fork - bw := bufio.NewWriterSize(targetFile, fileCopyBufSize) - if _, err = io.CopyN(bw, r, ffo.dataSize()); err != nil { - return err - } - if err := bw.Flush(); err != nil { + if _, err = io.Copy(targetFile, io.TeeReader(r, counterWriter)); err != nil { return err } @@ -59,47 +51,13 @@ func receiveFile(r io.Reader, targetFile, resForkFile, infoFork io.Writer) error return err } - bw = bufio.NewWriterSize(resForkFile, fileCopyBufSize) - _, err = io.CopyN(resForkFile, r, ffo.rsrcSize()) - if err != nil { - return err - } - if err := bw.Flush(); err != nil { + if _, err = io.Copy(resForkFile, io.TeeReader(r, counterWriter)); err != nil { return err } } return nil } -func sendFile(w io.Writer, r io.Reader, offset int) (err error) { - br := bufio.NewReader(r) - if _, err := br.Discard(offset); err != nil { - return err - } - - rSendBuffer := make([]byte, 1024) - for { - var bytesRead int - - if bytesRead, err = br.Read(rSendBuffer); err == io.EOF { - if _, err := w.Write(rSendBuffer[:bytesRead]); err != nil { - return err - } - return nil - } - if err != nil { - return err - } - // totalSent += int64(bytesRead) - - // fileTransfer.BytesSent += bytesRead - - if _, err := w.Write(rSendBuffer[:bytesRead]); err != nil { - return err - } - } -} - func (s *Server) bannerDownload(w io.Writer) error { bannerBytes, err := os.ReadFile(filepath.Join(s.ConfigDir, s.Config.BannerFile)) if err != nil { diff --git a/hotline/transfer_test.go b/hotline/transfer_test.go index fb7d39c..fb3e0da 100644 --- a/hotline/transfer_test.go +++ b/hotline/transfer_test.go @@ -188,7 +188,7 @@ func Test_receiveFile(t *testing.T) { targetFile := &bytes.Buffer{} resForkFile := &bytes.Buffer{} infoForkFile := &bytes.Buffer{} - err := receiveFile(tt.args.conn, targetFile, resForkFile, infoForkFile) + err := receiveFile(tt.args.conn, targetFile, resForkFile, infoForkFile, io.Discard) if !tt.wantErr(t, err, fmt.Sprintf("receiveFile(%v, %v, %v)", tt.args.conn, targetFile, resForkFile)) { return }