This commit is contained in:
2025-05-24 06:56:02 +04:00
parent 55a05a5571
commit b8c8da9e31
11 changed files with 517 additions and 119 deletions

View File

@@ -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
}