...
This commit is contained in:
		
							
								
								
									
										106
									
								
								pkg/herojobs/factory.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								pkg/herojobs/factory.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										63
									
								
								pkg/servers/heroagent/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								pkg/servers/heroagent/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										179
									
								
								pkg/servers/heroagent/factory.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								pkg/servers/heroagent/factory.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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, "") | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user