...
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