diff options
| -rw-r--r-- | hotline/config.go | 2 | ||||
| -rw-r--r-- | hotline/files.go | 8 | ||||
| -rw-r--r-- | internal/mobius/config.go | 19 | ||||
| -rw-r--r-- | internal/mobius/config_test.go | 95 | ||||
| -rw-r--r-- | internal/mobius/transaction_handlers.go | 3 | ||||
| -rw-r--r-- | internal/mobius/transaction_handlers_test.go | 50 |
6 files changed, 171 insertions, 6 deletions
diff --git a/hotline/config.go b/hotline/config.go index a7bd81e..75d5c4d 100644 --- a/hotline/config.go +++ b/hotline/config.go @@ -3,7 +3,7 @@ package hotline type Config struct { Name string `yaml:"Name" validate:"required,max=50"` // Name used for Tracker registration Description string `yaml:"Description" validate:"required,max=200"` // Description used for Tracker registration - BannerFile string `yaml:"BannerFile"` // Path to Banner jpg + BannerFile string `yaml:"BannerFile" validate:"omitempty,bannerext"` // Path to Banner jpg or gif FileRoot string `yaml:"FileRoot" validate:"required"` // Path to Files EnableTrackerRegistration bool `yaml:"EnableTrackerRegistration"` // Toggle Tracker Registration Trackers []string `yaml:"Trackers" validate:"dive,hostname_port"` // List of trackers that the server should register with diff --git a/hotline/files.go b/hotline/files.go index a1db329..e9f7b4f 100644 --- a/hotline/files.go +++ b/hotline/files.go @@ -12,7 +12,7 @@ import ( "strings" ) -func fileTypeFromFilename(filename string) fileType { +func FileTypeFromFilename(filename string) fileType { fileExt := strings.ToLower(filepath.Ext(filename)) ft, ok := fileTypes[fileExt] if ok { @@ -26,7 +26,7 @@ func fileTypeFromInfo(info fs.FileInfo) (ft fileType, err error) { ft.CreatorCode = "n/a " ft.TypeCode = "fldr" } else { - ft = fileTypeFromFilename(info.Name()) + ft = FileTypeFromFilename(info.Name()) } return ft, nil @@ -87,8 +87,8 @@ func GetFileNameList(path string, ignoreList []string) (fields []Field, err erro copy(fnwi.Creator[:], fileCreator) } else { binary.BigEndian.PutUint32(fnwi.FileSize[:], uint32(rFile.Size())) - copy(fnwi.Type[:], fileTypeFromFilename(rFile.Name()).TypeCode) - copy(fnwi.Creator[:], fileTypeFromFilename(rFile.Name()).CreatorCode) + copy(fnwi.Type[:], FileTypeFromFilename(rFile.Name()).TypeCode) + copy(fnwi.Creator[:], FileTypeFromFilename(rFile.Name()).CreatorCode) } } else if file.IsDir() { dir, err := os.ReadDir(filepath.Join(path, file.Name())) diff --git a/internal/mobius/config.go b/internal/mobius/config.go index d04b14d..c798e54 100644 --- a/internal/mobius/config.go +++ b/internal/mobius/config.go @@ -7,6 +7,7 @@ import ( "gopkg.in/yaml.v3" "os" "path/filepath" + "strings" ) var ConfigSearchOrder = []string{ @@ -28,7 +29,25 @@ func LoadConfig(path string) (*hotline.Config, error) { } validate := validator.New() + if err = validate.RegisterValidation("bannerext", func(fl validator.FieldLevel) bool { + filename := fl.Field().String() + if filename == "" { + return true // Allow empty since BannerFile is optional + } + ext := strings.ToLower(filepath.Ext(filename)) + return ext == ".jpg" || ext == ".jpeg" || ext == ".gif" + }); err != nil { + return nil, fmt.Errorf("register validation: %v", err) + } if err = validate.Struct(config); err != nil { + // Check if this is a BannerFile validation error and provide a better message + if validationErrs, ok := err.(validator.ValidationErrors); ok { + for _, fieldErr := range validationErrs { + if fieldErr.Field() == "BannerFile" && fieldErr.Tag() == "bannerext" { + return nil, fmt.Errorf("BannerFile must have a .jpg, .jpeg, or .gif extension (got: %s)", config.BannerFile) + } + } + } return nil, fmt.Errorf("validate config: %v", err) } diff --git a/internal/mobius/config_test.go b/internal/mobius/config_test.go new file mode 100644 index 0000000..b76b16d --- /dev/null +++ b/internal/mobius/config_test.go @@ -0,0 +1,95 @@ +package mobius + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestLoadConfig_InvalidBannerFileExtension(t *testing.T) { + // Create a temporary directory for test files + tmpDir, err := os.MkdirTemp("", "mobius-config-test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create a test config file with an invalid banner file extension + configContent := ` +Name: "Test Server" +Description: "Test Description" +BannerFile: "banner.png" +FileRoot: "files" +` + configPath := filepath.Join(tmpDir, "config.yaml") + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + t.Fatalf("Failed to write config file: %v", err) + } + + // Attempt to load the config + _, err = LoadConfig(configPath) + + // Verify that we get the improved error message + if err == nil { + t.Fatal("Expected error for invalid banner file extension, got nil") + } + + expectedMsg := "BannerFile must have a .jpg, .jpeg, or .gif extension (got: banner.png)" + if !strings.Contains(err.Error(), expectedMsg) { + t.Errorf("Expected error message to contain %q, got: %v", expectedMsg, err) + } +} + +func TestLoadConfig_ValidBannerFileExtensions(t *testing.T) { + tests := []struct { + name string + bannerFile string + }{ + {"jpg extension", "banner.jpg"}, + {"jpeg extension", "banner.jpeg"}, + {"gif extension", "banner.gif"}, + {"empty banner", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temporary directory for test files + tmpDir, err := os.MkdirTemp("", "mobius-config-test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create files subdirectory + filesDir := filepath.Join(tmpDir, "files") + if err := os.Mkdir(filesDir, 0755); err != nil { + t.Fatalf("Failed to create files dir: %v", err) + } + + // Create a test config file with a valid banner file extension + configContent := ` +Name: "Test Server" +Description: "Test Description" +BannerFile: "` + tt.bannerFile + `" +FileRoot: "files" +` + configPath := filepath.Join(tmpDir, "config.yaml") + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + t.Fatalf("Failed to write config file: %v", err) + } + + // Attempt to load the config + config, err := LoadConfig(configPath) + + // Verify that we don't get a validation error + if err != nil { + t.Errorf("Expected no error for %s, got: %v", tt.bannerFile, err) + } + + if config.BannerFile != tt.bannerFile { + t.Errorf("Expected BannerFile to be %q, got %q", tt.bannerFile, config.BannerFile) + } + }) + } +} diff --git a/internal/mobius/transaction_handlers.go b/internal/mobius/transaction_handlers.go index c5b1bbb..52ea29a 100644 --- a/internal/mobius/transaction_handlers.go +++ b/internal/mobius/transaction_handlers.go @@ -1095,7 +1095,8 @@ func HandleTranAgreed(cc *hotline.ClientConn, t *hotline.Transaction) (res []hot res = append(res, trans...) if cc.Server.Config.BannerFile != "" { - res = append(res, hotline.NewTransaction(hotline.TranServerBanner, cc.ID, hotline.NewField(hotline.FieldBannerType, []byte("JPEG")))) + bannerType := hotline.FileTypeFromFilename(cc.Server.Config.BannerFile).TypeCode + res = append(res, hotline.NewTransaction(hotline.TranServerBanner, cc.ID, hotline.NewField(hotline.FieldBannerType, []byte(bannerType)))) } res = append(res, cc.NewReply(t)) diff --git a/internal/mobius/transaction_handlers_test.go b/internal/mobius/transaction_handlers_test.go index 0d975a7..1c3fac0 100644 --- a/internal/mobius/transaction_handlers_test.go +++ b/internal/mobius/transaction_handlers_test.go @@ -2873,6 +2873,56 @@ func TestHandleTranAgreed(t *testing.T) { }, }, }, + { + name: "with gif banner", + args: args{ + cc: &hotline.ClientConn{ + Account: &hotline.Account{ + Access: func() hotline.AccessBitmap { + var bits hotline.AccessBitmap + bits.Set(hotline.AccessDisconUser) + bits.Set(hotline.AccessAnyName) + return bits + }()}, + Icon: []byte{0, 1}, + Flags: [2]byte{0, 1}, + Version: []byte{0, 1}, + ID: [2]byte{0, 1}, + Logger: NewTestLogger(), + Server: &hotline.Server{ + Config: hotline.Config{ + BannerFile: "Banner.gif", + }, + ClientMgr: func() *hotline.MockClientMgr { + m := hotline.MockClientMgr{} + m.On("List").Return([]*hotline.ClientConn{}, + ) + return &m + }(), + }, + }, + t: hotline.NewTransaction( + hotline.TranAgreed, [2]byte{}, + hotline.NewField(hotline.FieldUserName, []byte("username")), + hotline.NewField(hotline.FieldUserIconID, []byte{0, 1}), + hotline.NewField(hotline.FieldOptions, []byte{0, 0}), + ), + }, + wantRes: []hotline.Transaction{ + { + ClientID: [2]byte{0, 1}, + Type: [2]byte{0, 0x7a}, + Fields: []hotline.Field{ + hotline.NewField(hotline.FieldBannerType, []byte("GIFf")), + }, + }, + { + ClientID: [2]byte{0, 1}, + IsReply: 0x01, + Fields: []hotline.Field{}, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { |