diff --git a/cmd/heroagent/main.go b/cmd/heroagent/main.go index 2dd3d01..d34efdf 100644 --- a/cmd/heroagent/main.go +++ b/cmd/heroagent/main.go @@ -5,49 +5,72 @@ import ( "fmt" "log" "os" + "strconv" - "git.ourworld.tf/herocode/heroagent/pkg/heroagent" - "git.ourworld.tf/herocode/heroagent/pkg/servers/ui" // Import the new UI package + "git.ourworld.tf/herocode/heroagent/pkg/servers/heroagent" ) func main() { // Parse command-line flags - portFlag := flag.String("port", "", "Port to run the HeroLauncher on") - uiPortFlag := flag.String("uiport", "3000", "Port to run the UI server on") // New flag for UI port + redisPortFlag := flag.Int("redisport", 6378, "Port to run the Redis server on") + webdavPortFlag := flag.Int("webdavport", 9001, "Port to run the WebDAV server on") + uiPortFlag := flag.Int("uiport", 9002, "Port to run the UI server on") + + // Flags to enable/disable specific servers + enableRedisFlag := flag.Bool("redis", true, "Enable Redis server") + enableWebDAVFlag := flag.Bool("webdav", true, "Enable WebDAV server") + enableUIFlag := flag.Bool("ui", true, "Enable UI server") + flag.Parse() - // Initialize HeroLauncher with default configuration + // Initialize ServerFactory with default configuration config := heroagent.DefaultConfig() // Override with command-line flags if provided - if *portFlag != "" { - config.Port = *portFlag - } + config.Redis.TCPPort = *redisPortFlag + config.WebDAV.Config.TCPPort = *webdavPortFlag + config.UI.Port = strconv.Itoa(*uiPortFlag) + + // Set server enable flags + config.EnableRedis = *enableRedisFlag + config.EnableWebDAV = *enableWebDAVFlag + config.EnableUI = *enableUIFlag // Override with environment variables if provided - if port := os.Getenv("PORT"); port != "" { - config.Port = port + if redisPortStr := os.Getenv("REDIS_PORT"); redisPortStr != "" { + if port, err := strconv.Atoi(redisPortStr); err == nil { + config.Redis.TCPPort = port + } + } + if webdavPortStr := os.Getenv("WEBDAV_PORT"); webdavPortStr != "" { + if port, err := strconv.Atoi(webdavPortStr); err == nil { + config.WebDAV.Config.TCPPort = port + } + } + if uiPort := os.Getenv("UI_PORT"); uiPort != "" { + config.UI.Port = uiPort } - // Create HeroLauncher instance - launcher := heroagent.New(config) + // Create ServerFactory instance + factory := heroagent.New(config) - // Initialize and start the UI server in a new goroutine - go func() { - uiApp := ui.NewApp(ui.AppConfig{}) // Assuming default AppConfig is fine - uiPort := *uiPortFlag - if envUiPort := os.Getenv("UIPORT"); envUiPort != "" { - uiPort = envUiPort - } - fmt.Printf("Starting UI server on port %s...\n", uiPort) - if err := uiApp.Listen(":" + uiPort); err != nil { - log.Printf("Failed to start UI server: %v", err) // Use Printf to not exit main app - } - }() - - // Start the main HeroLauncher server - fmt.Printf("Starting HeroLauncher on port %s...\n", config.Port) - if err := launcher.Start(); err != nil { - log.Fatalf("Failed to start HeroLauncher: %v", err) + // Start all servers + fmt.Println("Starting HeroAgent servers...") + if err := factory.Start(); err != nil { + log.Fatalf("Failed to start servers: %v", err) } + + fmt.Printf("All servers started successfully:\n") + if config.EnableRedis { + fmt.Printf("- Redis server running on port %d\n", config.Redis.TCPPort) + } + if config.EnableWebDAV { + fmt.Printf("- WebDAV server running on port %d\n", config.WebDAV.Config.TCPPort) + } + if config.EnableUI { + fmt.Printf("- UI server running on port %s\n", config.UI.Port) + } + + // Keep the main goroutine running + select {} } diff --git a/go.mod b/go.mod index d35ef79..157256a 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.0 toolchain go1.23.6 require ( + github.com/go-redis/redis/v8 v8.11.5 github.com/gofiber/fiber/v2 v2.52.8 github.com/gofiber/template/jet/v2 v2.1.12 github.com/mholt/archiver/v3 v3.5.1 diff --git a/go.sum b/go.sum index 191edad..d58212e 100644 --- a/go.sum +++ b/go.sum @@ -54,11 +54,15 @@ github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj6 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4= github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc= @@ -141,6 +145,12 @@ github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7 github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode/v2 v2.0.0-beta.4 h1:sdiJxQdPjECn2lh9nLFFhgLCf+0ulDU5rODbtERTlUY= github.com/nwaples/rardecode/v2 v2.0.0-beta.4/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= @@ -376,7 +386,11 @@ google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/heroagent b/heroagent new file mode 100755 index 0000000..c131b8a Binary files /dev/null and b/heroagent differ diff --git a/pkg/herojobs/factory.go b/pkg/herojobs/factory.go new file mode 100644 index 0000000..1e661b0 --- /dev/null +++ b/pkg/herojobs/factory.go @@ -0,0 +1,106 @@ +package herojobs + +import ( + "context" + "fmt" + "time" + + "github.com/go-redis/redis/v8" +) + +const ( + defaultRedisURL = "redis://localhost:6379/0" +) + +// Factory manages job-related operations, including Redis connectivity and watchdog. +type Factory struct { + redisClient *redis.Client + // Add other fields as needed, e.g., for watchdog +} + +// NewFactory creates a new Factory instance. +// It takes a redisURL string; if empty, it defaults to defaultRedisURL. +func NewFactory(redisURL string) (*Factory, error) { + if redisURL == "" { + redisURL = defaultRedisURL + } + + opt, err := redis.ParseURL(redisURL) + if err != nil { + return nil, fmt.Errorf("invalid redis URL: %w", err) + } + + client := redis.NewClient(opt) + + // Check connection to Redis + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err = client.Ping(ctx).Result() + if err != nil { + return nil, fmt.Errorf("failed to connect to redis at %s: %w", redisURL, err) + } + + fmt.Printf("Successfully connected to Redis at %s\n", redisURL) + + factory := &Factory{ + redisClient: client, + } + + // TODO: Properly start the watchdog here + fmt.Println("Watchdog placeholder: Watchdog would be started here.") + + return factory, nil +} + +// Close closes the Redis client connection. +func (f *Factory) Close() error { + if f.redisClient != nil { + return f.redisClient.Close() + } + return nil +} + +// GetJob retrieves a job by its ID from Redis. +func (f *Factory) GetJob(ctx context.Context, jobID string) (string, error) { + // Example: Assuming jobs are stored as string values + val, err := f.redisClient.Get(ctx, jobID).Result() + if err == redis.Nil { + return "", fmt.Errorf("job with ID %s not found", jobID) + } else if err != nil { + return "", fmt.Errorf("failed to get job %s from redis: %w", jobID, err) + } + return val, nil +} + +// ListJobs lists all job IDs (or a subset) from Redis. +// This is a simplified example; real-world job listing might involve more complex data structures. +func (f *Factory) ListJobs(ctx context.Context) ([]string, error) { + // Example: List all keys that might represent jobs. + // In a real application, you'd likely use specific Redis data structures (e.g., sorted sets, hashes) + // to manage jobs more efficiently and avoid scanning all keys. + keys, err := f.redisClient.Keys(ctx, "job:*").Result() // Assuming job keys are prefixed with "job:" + if err != nil { + return nil, fmt.Errorf("failed to list jobs from redis: %w", err) + } + return keys, nil +} + +// AddJob adds a new job to Redis. +func (f *Factory) AddJob(ctx context.Context, jobID string, jobData string) error { + // Example: Store job data as a string + err := f.redisClient.Set(ctx, jobID, jobData, 0).Err() // 0 for no expiration + if err != nil { + return fmt.Errorf("failed to add job %s to redis: %w", jobID, err) + } + return nil +} + +// DeleteJob deletes a job from Redis. +func (f *Factory) DeleteJob(ctx context.Context, jobID string) error { + _, err := f.redisClient.Del(ctx, jobID).Result() + if err != nil { + return fmt.Errorf("failed to delete job %s from redis: %w", jobID, err) + } + return nil +} diff --git a/pkg/servers/heroagent/config.go b/pkg/servers/heroagent/config.go new file mode 100644 index 0000000..6f14221 --- /dev/null +++ b/pkg/servers/heroagent/config.go @@ -0,0 +1,63 @@ +package heroagent + +import ( + "git.ourworld.tf/herocode/heroagent/pkg/servers/ui" + "git.ourworld.tf/herocode/heroagent/pkg/servers/webdavserver" +) + +// Config holds the configuration for the HeroAgent server factory +type Config struct { + // Redis server configuration + Redis RedisConfig + + // WebDAV server configuration + WebDAV WebDAVConfig + + // UI server configuration + UI UIConfig + + // Enable/disable specific servers + EnableRedis bool + EnableWebDAV bool + EnableUI bool +} + +// RedisConfig holds the configuration for the Redis server +type RedisConfig struct { + TCPPort int + UnixSocketPath string +} + +// WebDAVConfig holds the configuration for the WebDAV server +type WebDAVConfig struct { + // Use webdavserver.Config directly + Config webdavserver.Config +} + +// UIConfig holds the configuration for the UI server +type UIConfig struct { + // UI server configuration + Port string + // Any additional UI-specific configuration + AppConfig ui.AppConfig +} + +// DefaultConfig returns the default configuration for the HeroAgent +func DefaultConfig() Config { + return Config{ + Redis: RedisConfig{ + TCPPort: 6378, + UnixSocketPath: "/tmp/redis.sock", + }, + WebDAV: WebDAVConfig{ + Config: webdavserver.DefaultConfig(), + }, + UI: UIConfig{ + Port: "9001", // Port is a string in UIConfig + AppConfig: ui.AppConfig{}, + }, + EnableRedis: true, + EnableWebDAV: true, + EnableUI: true, + } +} diff --git a/pkg/servers/heroagent/factory.go b/pkg/servers/heroagent/factory.go new file mode 100644 index 0000000..e67bd21 --- /dev/null +++ b/pkg/servers/heroagent/factory.go @@ -0,0 +1,179 @@ +package heroagent + +import ( + "fmt" + "log" + "sync" + + "git.ourworld.tf/herocode/heroagent/pkg/servers/redisserver" + "git.ourworld.tf/herocode/heroagent/pkg/servers/ui" + "git.ourworld.tf/herocode/heroagent/pkg/servers/webdavserver" + "github.com/gofiber/fiber/v2" +) + +// ServerFactory manages the lifecycle of all servers +type ServerFactory struct { + config Config + + // Server instances + redisServer *redisserver.Server + webdavServer *webdavserver.Server + uiApp *AppInstance + + // Control channels + stopCh chan struct{} + wg sync.WaitGroup +} + +// AppInstance wraps the UI app and its listening status +type AppInstance struct { + App *fiber.App + Port string +} + +// New creates a new ServerFactory with the given configuration +func New(config Config) *ServerFactory { + return &ServerFactory{ + config: config, + stopCh: make(chan struct{}), + } +} + +// Start initializes and starts all enabled servers +func (f *ServerFactory) Start() error { + log.Println("Starting HeroAgent ServerFactory...") + + // Start Redis server if enabled + if f.config.EnableRedis { + if err := f.startRedisServer(); err != nil { + return fmt.Errorf("failed to start Redis server: %w", err) + } + } + + // Start WebDAV server if enabled + if f.config.EnableWebDAV { + if err := f.startWebDAVServer(); err != nil { + return fmt.Errorf("failed to start WebDAV server: %w", err) + } + } + + // Start UI server if enabled + if f.config.EnableUI { + if err := f.startUIServer(); err != nil { + return fmt.Errorf("failed to start UI server: %w", err) + } + } + + log.Println("All servers started successfully") + return nil +} + +// Stop gracefully stops all running servers +func (f *ServerFactory) Stop() error { + log.Println("Stopping all servers...") + + // Signal all goroutines to stop + close(f.stopCh) + + // Stop WebDAV server if it's running + if f.webdavServer != nil { + if err := f.webdavServer.Stop(); err != nil { + log.Printf("Error stopping WebDAV server: %v", err) + } + } + + // Wait for all goroutines to finish + f.wg.Wait() + + log.Println("All servers stopped") + return nil +} + +// startRedisServer initializes and starts the Redis server +func (f *ServerFactory) startRedisServer() error { + log.Println("Starting Redis server...") + + // Create Redis server configuration + redisConfig := redisserver.ServerConfig{ + TCPPort: f.config.Redis.TCPPort, + UnixSocketPath: f.config.Redis.UnixSocketPath, + } + + // Create and start Redis server + f.redisServer = redisserver.NewServer(redisConfig) + + log.Printf("Redis server started on port %d and socket %s", + redisConfig.TCPPort, redisConfig.UnixSocketPath) + return nil +} + +// startWebDAVServer initializes and starts the WebDAV server +func (f *ServerFactory) startWebDAVServer() error { + log.Println("Starting WebDAV server...") + + // Create WebDAV server + webdavServer, err := webdavserver.NewServer(f.config.WebDAV.Config) + if err != nil { + return fmt.Errorf("failed to create WebDAV server: %w", err) + } + + f.webdavServer = webdavServer + + // Start WebDAV server in a goroutine + f.wg.Add(1) + go func() { + defer f.wg.Done() + + // Start the server + if err := webdavServer.Start(); err != nil { + log.Printf("WebDAV server error: %v", err) + } + }() + + log.Printf("WebDAV server started on port %d", f.config.WebDAV.Config.TCPPort) + return nil +} + +// startUIServer initializes and starts the UI server +func (f *ServerFactory) startUIServer() error { + log.Println("Starting UI server...") + + // Create UI app + uiApp := ui.NewApp(f.config.UI.AppConfig) + + // Store UI app instance + f.uiApp = &AppInstance{ + App: uiApp, + Port: f.config.UI.Port, + } + + // Start UI server in a goroutine + f.wg.Add(1) + go func() { + defer f.wg.Done() + + // Start the server + addr := ":" + f.config.UI.Port + log.Printf("UI server listening on %s", addr) + if err := uiApp.Listen(addr); err != nil { + log.Printf("UI server error: %v", err) + } + }() + + return nil +} + +// GetRedisServer returns the Redis server instance +func (f *ServerFactory) GetRedisServer() *redisserver.Server { + return f.redisServer +} + +// GetWebDAVServer returns the WebDAV server instance +func (f *ServerFactory) GetWebDAVServer() *webdavserver.Server { + return f.webdavServer +} + +// GetUIApp returns the UI app instance +func (f *ServerFactory) GetUIApp() *AppInstance { + return f.uiApp +} diff --git a/pkg/servers/redisserver/cmd/redischeck/main.go b/pkg/servers/redisserver/cmd/redischeck/main.go index cff721e..7df5651 100644 --- a/pkg/servers/redisserver/cmd/redischeck/main.go +++ b/pkg/servers/redisserver/cmd/redischeck/main.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "os" + "strconv" "strings" "sync" "time" @@ -16,7 +17,7 @@ import ( func main() { // Parse command line flags - tcpPort := flag.String("tcp-port", "7777", "Redis server TCP port") + tcpPortStr := flag.String("tcp-port", "7777", "Redis server TCP port") unixSocket := flag.String("unix-socket", "/tmp/redis-test.sock", "Redis server Unix domain socket path") username := flag.String("user", "jan", "Username to check") mailbox := flag.String("mailbox", "inbox", "Mailbox to check") @@ -24,8 +25,13 @@ func main() { dbNum := flag.Int("db", 0, "Redis database number") flag.Parse() + tcpPort, err := strconv.Atoi(*tcpPortStr) + if err != nil { + log.Fatalf("Invalid TCP port: %v", err) + } + // Start Redis server in a goroutine - log.Printf("Starting Redis server on TCP port %s and Unix socket %s", *tcpPort, *unixSocket) + log.Printf("Starting Redis server on TCP port %d and Unix socket %s", tcpPort, *unixSocket) // Create a wait group to ensure the server is started before testing var wg sync.WaitGroup @@ -44,7 +50,7 @@ func main() { // Start the Redis server in a goroutine go func() { // Create a new server instance - _ = redisserver.NewServer(redisserver.ServerConfig{TCPPort: *tcpPort, UnixSocketPath: *unixSocket}) + _ = redisserver.NewServer(redisserver.ServerConfig{TCPPort: tcpPort, UnixSocketPath: *unixSocket}) // Signal that the server is ready wg.Done() @@ -61,7 +67,7 @@ func main() { // Test TCP connection log.Println("Testing TCP connection") - tcpAddr := fmt.Sprintf("localhost:%s", *tcpPort) + tcpAddr := fmt.Sprintf("localhost:%d", tcpPort) testRedisConnection(tcpAddr, username, mailbox, debug, dbNum) // Test Unix socket connection if supported diff --git a/pkg/servers/redisserver/cmd/redistest/main.go b/pkg/servers/redisserver/cmd/redistest/main.go index 119e4b1..cba673c 100644 --- a/pkg/servers/redisserver/cmd/redistest/main.go +++ b/pkg/servers/redisserver/cmd/redistest/main.go @@ -62,14 +62,19 @@ func (ts *TestSuite) PrintResults() { func main() { // Parse command line flags - tcpPort := flag.String("tcp-port", "7777", "Redis server TCP port") + tcpPortStr := flag.String("tcp-port", "7777", "Redis server TCP port") unixSocket := flag.String("unix-socket", "/tmp/redis-test.sock", "Redis server Unix domain socket path") debug := flag.Bool("debug", false, "Enable debug output") dbNum := flag.Int("db", 0, "Redis database number") flag.Parse() + tcpPortInt, err := strconv.Atoi(*tcpPortStr) + if err != nil { + log.Fatalf("Invalid TCP port: %v", err) + } + // Start Redis server in a goroutine - log.Printf("Starting Redis server on TCP port %s and Unix socket %s", *tcpPort, *unixSocket) + log.Printf("Starting Redis server on TCP port %d and Unix socket %s", tcpPortInt, *unixSocket) // Create a wait group to ensure the server is started before testing var wg sync.WaitGroup @@ -88,7 +93,7 @@ func main() { // Start the Redis server in a goroutine go func() { // Create a new server instance - _ = redisserver.NewServer(redisserver.ServerConfig{TCPPort: *tcpPort, UnixSocketPath: *unixSocket}) + _ = redisserver.NewServer(redisserver.ServerConfig{TCPPort: tcpPortInt, UnixSocketPath: *unixSocket}) // Signal that the server is ready wg.Done() @@ -105,7 +110,7 @@ func main() { // Test TCP connection log.Println("Testing TCP connection") - tcpAddr := fmt.Sprintf("localhost:%s", *tcpPort) + tcpAddr := fmt.Sprintf("localhost:%d", tcpPortInt) runTests(tcpAddr, *debug, *dbNum) // Test Unix socket connection if supported diff --git a/pkg/servers/redisserver/factory.go b/pkg/servers/redisserver/factory.go index 1410549..c3d7695 100644 --- a/pkg/servers/redisserver/factory.go +++ b/pkg/servers/redisserver/factory.go @@ -1,6 +1,7 @@ package redisserver import ( + "strconv" "sync" "time" ) @@ -20,7 +21,7 @@ type Server struct { } type ServerConfig struct { - TCPPort string + TCPPort int UnixSocketPath string } @@ -38,8 +39,8 @@ func NewServer(config ServerConfig) *Server { go s.cleanupExpiredKeys() // Start TCP server if port is provided - if config.TCPPort != "" { - tcpAddr := ":" + config.TCPPort + if config.TCPPort != 0 { + tcpAddr := ":" + strconv.Itoa(config.TCPPort) go s.startRedisServer(tcpAddr, "") } diff --git a/pkg/servers/webdavserver/server.go b/pkg/servers/webdavserver/server.go index 5863183..fe78f6a 100644 --- a/pkg/servers/webdavserver/server.go +++ b/pkg/servers/webdavserver/server.go @@ -26,22 +26,22 @@ import ( // Config holds the configuration for the WebDAV server type Config struct { - Host string - Port int - BasePath string - FileSystem string - ReadTimeout time.Duration - WriteTimeout time.Duration - DebugMode bool - UseAuth bool - Username string - Password string - UseHTTPS bool - CertFile string - KeyFile string - AutoGenerateCerts bool - CertValidityDays int - CertOrganization string + Host string + TCPPort int + BasePath string + FileSystem string + ReadTimeout time.Duration + WriteTimeout time.Duration + DebugMode bool + UseAuth bool + Username string + Password string + UseHTTPS bool + CertFile string + KeyFile string + AutoGenerateCerts bool + CertValidityDays int + CertOrganization string } // Server represents the WebDAV server @@ -74,18 +74,18 @@ func (rw *responseWrapper) Write(b []byte) (int, error) { // NewServer creates a new WebDAV server func NewServer(config Config) (*Server, error) { - log.Printf("Creating new WebDAV server with config: host=%s, port=%d, basePath=%s, fileSystem=%s, debug=%v, auth=%v, https=%v", - config.Host, config.Port, config.BasePath, config.FileSystem, config.DebugMode, config.UseAuth, config.UseHTTPS) + log.Printf("Creating new WebDAV server with config: host=%s, TCPPort=%d, basePath=%s, fileSystem=%s, debug=%v, auth=%v, https=%v", + config.Host, config.TCPPort, config.BasePath, config.FileSystem, config.DebugMode, config.UseAuth, config.UseHTTPS) // Ensure the file system directory exists if err := os.MkdirAll(config.FileSystem, 0755); err != nil { log.Printf("ERROR: Failed to create file system directory %s: %v", config.FileSystem, err) return nil, fmt.Errorf("failed to create file system directory: %w", err) } - + // Log the file system path log.Printf("Using file system path: %s", config.FileSystem) - + // Create debug logger function debugLog := func(format string, v ...interface{}) { if config.DebugMode { @@ -103,7 +103,7 @@ func NewServer(config Config) (*Server, error) { } else { log.Printf("WebDAV: %s %s", r.Method, r.URL.Path) } - + // Additional debug logging if config.DebugMode { log.Printf("[WebDAV DEBUG] Request Headers: %v", r.Header) @@ -115,7 +115,7 @@ func NewServer(config Config) (*Server, error) { // Create HTTP server httpServer := &http.Server{ - Addr: fmt.Sprintf("%s:%d", config.Host, config.Port), + Addr: fmt.Sprintf("%s:%d", config.Host, config.TCPPort), ReadTimeout: config.ReadTimeout, WriteTimeout: config.WriteTimeout, } @@ -141,15 +141,15 @@ func (s *Server) Start() error { s.debugLog("Received request: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr) s.debugLog("Request Protocol: %s", r.Proto) s.debugLog("User-Agent: %s", r.UserAgent()) - + // Log all request headers for name, values := range r.Header { s.debugLog("Header: %s = %s", name, values) } - + // Log request depth (important for WebDAV) s.debugLog("Depth header: %s", r.Header.Get("Depth")) - + // Add CORS headers w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, OPTIONS, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE") @@ -162,32 +162,32 @@ func (s *Server) Start() error { w.Header().Set("DAV", "1, 2") w.Header().Set("MS-Author-Via", "DAV") w.Header().Set("Allow", "OPTIONS, GET, HEAD, POST, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE") - + // Check if this is a macOS WebDAV client - isMacOSClient := strings.Contains(r.UserAgent(), "WebDAVFS") || - strings.Contains(r.UserAgent(), "WebDAVLib") || + isMacOSClient := strings.Contains(r.UserAgent(), "WebDAVFS") || + strings.Contains(r.UserAgent(), "WebDAVLib") || strings.Contains(r.UserAgent(), "Darwin") - + if isMacOSClient { s.debugLog("Detected macOS WebDAV client OPTIONS request, adding macOS-specific headers") // These headers help macOS Finder with WebDAV compatibility w.Header().Set("X-Dav-Server", "HeroLauncher WebDAV Server") } - + w.WriteHeader(http.StatusOK) return } - + // Handle authentication if enabled if s.config.UseAuth { s.debugLog("Authentication required for request") auth := r.Header.Get("Authorization") - + // Check if this is a macOS WebDAV client - isMacOSClient := strings.Contains(r.UserAgent(), "WebDAVFS") || - strings.Contains(r.UserAgent(), "WebDAVLib") || + isMacOSClient := strings.Contains(r.UserAgent(), "WebDAVFS") || + strings.Contains(r.UserAgent(), "WebDAVLib") || strings.Contains(r.UserAgent(), "Darwin") - + // Special handling for OPTIONS requests from macOS clients if r.Method == "OPTIONS" && isMacOSClient { s.debugLog("Detected macOS WebDAV client OPTIONS request, allowing without auth") @@ -196,28 +196,28 @@ func (s *Server) Start() error { w.Header().Set("WWW-Authenticate", "Basic realm=\"WebDAV Server\"") return } - + if auth == "" { s.debugLog("No Authorization header provided for non-OPTIONS request") w.Header().Set("WWW-Authenticate", "Basic realm=\"WebDAV Server\"") http.Error(w, "Unauthorized", http.StatusUnauthorized) return } - + // Parse the authentication header if !strings.HasPrefix(auth, "Basic ") { s.debugLog("Invalid Authorization header format: %s", auth) http.Error(w, "Invalid authorization header", http.StatusBadRequest) return } - + payload, err := base64.StdEncoding.DecodeString(auth[6:]) if err != nil { s.debugLog("Failed to decode Authorization header: %v, raw header: %s", err, auth) http.Error(w, "Invalid authorization header", http.StatusBadRequest) return } - + pair := strings.SplitN(string(payload), ":", 2) if len(pair) != 2 { s.debugLog("Invalid credential format: could not split into username:password") @@ -225,17 +225,17 @@ func (s *Server) Start() error { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } - + // Log username for debugging (don't log password) s.debugLog("Received credentials for user: %s", pair[0]) - + if pair[0] != s.config.Username || pair[1] != s.config.Password { s.debugLog("Invalid credentials provided, expected user: %s", s.config.Username) w.Header().Set("WWW-Authenticate", "Basic realm=\"WebDAV Server\"") http.Error(w, "Unauthorized", http.StatusUnauthorized) return } - + s.debugLog("Authentication successful for user: %s", pair[0]) } @@ -252,17 +252,17 @@ func (s *Server) Start() error { } // Add macOS-specific headers for better compatibility - isMacOSClient := strings.Contains(r.UserAgent(), "WebDAVFS") || - strings.Contains(r.UserAgent(), "WebDAVLib") || + isMacOSClient := strings.Contains(r.UserAgent(), "WebDAVFS") || + strings.Contains(r.UserAgent(), "WebDAVLib") || strings.Contains(r.UserAgent(), "Darwin") - + if isMacOSClient { s.debugLog("Adding macOS-specific headers for better compatibility") // These headers help macOS Finder with WebDAV compatibility w.Header().Set("MS-Author-Via", "DAV") w.Header().Set("X-Dav-Server", "HeroLauncher WebDAV Server") w.Header().Set("DAV", "1, 2") - + // Special handling for PROPFIND requests from macOS if r.Method == "PROPFIND" { s.debugLog("Handling macOS PROPFIND request with special compatibility") @@ -281,7 +281,7 @@ func (s *Server) Start() error { // Log response details s.debugLog("Response status: %d", responseWrapper.statusCode) s.debugLog("Response content type: %s", w.Header().Get("Content-Type")) - + // Log detailed information for debugging connection issues if responseWrapper.statusCode >= 400 { s.debugLog("ERROR: WebDAV request failed with status %d", responseWrapper.statusCode) @@ -303,24 +303,24 @@ func (s *Server) Start() error { log.Printf("ERROR: HTTPS enabled but certificate or key file not provided and auto-generation is disabled") return fmt.Errorf("HTTPS enabled but certificate or key file not provided and auto-generation is disabled") } - + // Auto-generate certificates if needed - if (s.config.CertFile == "" || s.config.KeyFile == "" || - !fileExists(s.config.CertFile) || !fileExists(s.config.KeyFile)) && + if (s.config.CertFile == "" || s.config.KeyFile == "" || + !fileExists(s.config.CertFile) || !fileExists(s.config.KeyFile)) && s.config.AutoGenerateCerts { - + s.debugLog("Certificate files not found, auto-generating...") - + // Get base directory from the file system path baseDir := filepath.Dir(s.config.FileSystem) - + // Create certificates directory if it doesn't exist certsDir := filepath.Join(baseDir, "certificates") if err := os.MkdirAll(certsDir, 0755); err != nil { log.Printf("ERROR: Failed to create certificates directory: %v", err) return fmt.Errorf("failed to create certificates directory: %w", err) } - + // Set default certificate paths if not provided if s.config.CertFile == "" { s.config.CertFile = filepath.Join(certsDir, "webdav.crt") @@ -328,44 +328,44 @@ func (s *Server) Start() error { if s.config.KeyFile == "" { s.config.KeyFile = filepath.Join(certsDir, "webdav.key") } - + // Generate certificates if err := generateCertificate( - s.config.CertFile, - s.config.KeyFile, - s.config.CertOrganization, + s.config.CertFile, + s.config.KeyFile, + s.config.CertOrganization, s.config.CertValidityDays, s.debugLog, ); err != nil { log.Printf("ERROR: Failed to generate certificates: %v", err) return fmt.Errorf("failed to generate certificates: %w", err) } - - log.Printf("Successfully generated self-signed certificates at %s and %s", + + log.Printf("Successfully generated self-signed certificates at %s and %s", s.config.CertFile, s.config.KeyFile) } - + // Verify certificate files exist if !fileExists(s.config.CertFile) || !fileExists(s.config.KeyFile) { - log.Printf("ERROR: Certificate files not found at %s and/or %s", + log.Printf("ERROR: Certificate files not found at %s and/or %s", s.config.CertFile, s.config.KeyFile) return fmt.Errorf("certificate files not found") } - + // Configure TLS tlsConfig := &tls.Config{ MinVersion: tls.VersionTLS12, } s.httpServer.TLSConfig = tlsConfig - - log.Printf("Starting WebDAV server with HTTPS on %s using certificates: %s, %s", + + log.Printf("Starting WebDAV server with HTTPS on %s using certificates: %s, %s", s.httpServer.Addr, s.config.CertFile, s.config.KeyFile) err = s.httpServer.ListenAndServeTLS(s.config.CertFile, s.config.KeyFile) } else { log.Printf("Starting WebDAV server with HTTP on %s", s.httpServer.Addr) err = s.httpServer.ListenAndServe() } - + if err != nil && err != http.ErrServerClosed { log.Printf("ERROR: WebDAV server failed to start: %v", err) return err @@ -389,10 +389,10 @@ func (s *Server) Stop() error { func DefaultConfig() Config { // Use system temp directory as default base path defaultBasePath := filepath.Join(os.TempDir(), "heroagent") - + return Config{ Host: "0.0.0.0", - Port: 9999, + TCPPort: 9999, BasePath: "/", FileSystem: defaultBasePath, ReadTimeout: 30 * time.Second, @@ -421,24 +421,24 @@ func fileExists(filename string) bool { // generateCertificate creates a self-signed TLS certificate and key func generateCertificate(certFile, keyFile, organization string, validityDays int, debugLog func(format string, args ...interface{})) error { - debugLog("Generating self-signed certificate: certFile=%s, keyFile=%s, organization=%s, validityDays=%d", + debugLog("Generating self-signed certificate: certFile=%s, keyFile=%s, organization=%s, validityDays=%d", certFile, keyFile, organization, validityDays) - + // Generate private key privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return fmt.Errorf("failed to generate private key: %w", err) } - + // Prepare certificate template notBefore := time.Now() notAfter := notBefore.Add(time.Duration(validityDays) * 24 * time.Hour) - + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { return fmt.Errorf("failed to generate serial number: %w", err) } - + template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ @@ -453,36 +453,36 @@ func generateCertificate(certFile, keyFile, organization string, validityDays in IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, DNSNames: []string{"localhost"}, } - + // Create certificate derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) if err != nil { return fmt.Errorf("failed to create certificate: %w", err) } - + // Write certificate to file certOut, err := os.Create(certFile) if err != nil { return fmt.Errorf("failed to open %s for writing: %w", certFile, err) } defer certOut.Close() - + if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { return fmt.Errorf("failed to write certificate to file: %w", err) } - + // Write private key to file keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return fmt.Errorf("failed to open %s for writing: %w", keyFile, err) } defer keyOut.Close() - + privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)} if err := pem.Encode(keyOut, privateKeyPEM); err != nil { return fmt.Errorf("failed to write private key to file: %w", err) } - + debugLog("Successfully generated self-signed certificate valid for %d days", validityDays) return nil }