diff options
| author | Collin Williams <96917990+bluedragon1221@users.noreply.github.com> | 2026-05-22 10:23:58 -0500 |
|---|---|---|
| committer | Collin Williams <96917990+bluedragon1221@users.noreply.github.com> | 2026-05-22 10:24:06 -0500 |
| commit | 35af4bab99da94845ebd6e9b2efae6a544d39b35 (patch) | |
| tree | 47c6f76b47f1012932729c9f693316df1512197a /modules/services/nixos/ganyupload/main.go | |
| parent | 2e0c6ccbbc76cc36e92128aea44e4bdd4e5c3d0a (diff) | |
LOTS OF TEMP STUFF
Diffstat (limited to 'modules/services/nixos/ganyupload/main.go')
| -rw-r--r-- | modules/services/nixos/ganyupload/main.go | 212 |
1 files changed, 205 insertions, 7 deletions
diff --git a/modules/services/nixos/ganyupload/main.go b/modules/services/nixos/ganyupload/main.go index 6aa219e..75852c2 100644 --- a/modules/services/nixos/ganyupload/main.go +++ b/modules/services/nixos/ganyupload/main.go @@ -2,11 +2,14 @@ package main import ( _ "embed" + "fmt" + "html" "io" "log" "net/http" "os" "path/filepath" + "sort" "strings" ) @@ -15,6 +18,42 @@ var readme string var uploadDir = "." +// cleanPath safely cleans and validates a path to prevent traversal attacks +func cleanPath(filename string) (string, error) { + // Remove leading slash + filename = strings.TrimPrefix(filename, "/") + if filename == "" { + return "", nil + } + + // Clean the path + filename = filepath.Clean(filename) + + // Reject if it tries to go up + if strings.HasPrefix(filename, "..") || strings.Contains(filename, "/../") { + return "", fmt.Errorf("invalid filename: path traversal not allowed") + } + + return filename, nil +} + +// isPathSafe checks that the resolved fullPath stays within uploadDir +func isPathSafe(uploadDir, fullPath string) bool { + // Resolve both paths to absolute to catch symlink attacks + absUploadDir, err := filepath.Abs(uploadDir) + if err != nil { + return false + } + absFullPath, err := filepath.Abs(fullPath) + if err != nil { + return false + } + + // Ensure the resolved path starts with uploadDir + return strings.HasPrefix(absFullPath, absUploadDir+string(filepath.Separator)) || + absFullPath == absUploadDir +} + func handleRoot(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) @@ -32,27 +71,37 @@ func handleUpload(w http.ResponseWriter, r *http.Request) { return } + // Handle GET requests for downloads and directory listing + if r.Method == http.MethodGet { + handleDownload(w, r) + return + } + if r.Method != http.MethodPut { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } // Extract filename from URL path, preventing path traversal - filename := strings.TrimPrefix(r.URL.Path, "/") - if filename == "" { - http.Error(w, "filename required", http.StatusBadRequest) + filename, err := cleanPath(r.URL.Path) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) return } - // Prevent path traversal - filename = filepath.Clean(filename) - if strings.HasPrefix(filename, "..") { - http.Error(w, "invalid filename", http.StatusBadRequest) + if filename == "" { + http.Error(w, "filename required", http.StatusBadRequest) return } fullPath := filepath.Join(uploadDir, filename) + // Verify the path is safe + if !isPathSafe(uploadDir, fullPath) { + http.Error(w, "invalid path", http.StatusBadRequest) + return + } + // Ensure the directory exists dir := filepath.Dir(fullPath) if err := os.MkdirAll(dir, 0755); err != nil { @@ -85,6 +134,155 @@ func handleUpload(w http.ResponseWriter, r *http.Request) { log.Printf("uploaded %s (%d bytes)", filename, written) } +func handleDownload(w http.ResponseWriter, r *http.Request) { + // Extract filename from URL path + filename, err := cleanPath(r.URL.Path) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + fullPath := filepath.Join(uploadDir, filename) + + // Verify the path is safe + if !isPathSafe(uploadDir, fullPath) { + http.Error(w, "invalid path", http.StatusBadRequest) + return + } + + fileInfo, err := os.Stat(fullPath) + if err != nil { + if os.IsNotExist(err) { + http.Error(w, "not found", http.StatusNotFound) + } else { + log.Printf("failed to stat: %v", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + } + return + } + + // If it's a directory, list its contents + if fileInfo.IsDir() { + handleDirList(w, r, uploadDir, filename, fullPath) + return + } + + // Serve the file + http.ServeFile(w, r, fullPath) +} + +func handleDirList(w http.ResponseWriter, r *http.Request, uploadDir, relPath, fullPath string) { + entries, err := os.ReadDir(fullPath) + if err != nil { + log.Printf("failed to read directory: %v", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + // Sort entries by name + sort.Slice(entries, func(i, j int) bool { + return entries[i].Name() < entries[j].Name() + }) + + // Build breadcrumb navigation + pathParts := strings.Split(strings.Trim(relPath, "/"), "/") + if relPath == "" { + pathParts = []string{} + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, `<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Directory: %s</title> + <style> + body { font-family: monospace; margin: 2rem; } + h1 { margin-bottom: 0.5rem; } + .breadcrumb { margin-bottom: 2rem; font-size: 0.9rem; } + .breadcrumb a { text-decoration: none; color: #0066cc; } + .breadcrumb a:hover { text-decoration: underline; } + table { border-collapse: collapse; width: 100%%; } + td { padding: 0.5rem; border-bottom: 1px solid #ccc; } + td:first-child { text-align: left; } + td:last-child { text-align: right; color: #666; } + a { text-decoration: none; color: #0066cc; } + a:hover { text-decoration: underline; } + .directory { color: #0066cc; font-weight: bold; } + </style> +</head> +<body> + <h1>Directory: %s</h1> +`, html.EscapeString(relPath), html.EscapeString(relPath)) + + // Breadcrumb navigation + fmt.Fprint(w, ` <div class="breadcrumb">`) + fmt.Fprintf(w, `<a href="/">home</a>`) + if len(pathParts) > 0 { + accumulated := "" + for _, part := range pathParts { + accumulated = accumulated + "/" + part + fmt.Fprintf(w, ` / <a href="%s">%s</a>`, html.EscapeString(accumulated), html.EscapeString(part)) + } + } + fmt.Fprint(w, `</div>`) + + // Directory listing + fmt.Fprint(w, ` <table> +`) + for _, entry := range entries { + name := entry.Name() + safeName := html.EscapeString(name) + + if entry.IsDir() { + // For directories, add trailing slash to the link + linkPath := relPath + "/" + name + if relPath == "" { + linkPath = "/" + name + } + linkPath = strings.TrimPrefix(linkPath, "/") + fmt.Fprintf(w, ` <tr><td><a href="/%s" class="directory">%s/</a></td><td>-</td></tr> +`, html.EscapeString(linkPath), safeName) + } else { + info, _ := entry.Info() + linkPath := relPath + "/" + name + if relPath == "" { + linkPath = "/" + name + } + linkPath = strings.TrimPrefix(linkPath, "/") + size := formatSize(info.Size()) + fmt.Fprintf(w, ` <tr><td><a href="/%s">%s</a></td><td>%s</td></tr> +`, html.EscapeString(linkPath), safeName, size) + } + } + fmt.Fprint(w, ` </table> +</body> +</html> +`) +} + +func formatSize(bytes int64) string { + const ( + KB = 1024 + MB = KB * 1024 + GB = MB * 1024 + ) + + switch { + case bytes < KB: + return fmt.Sprintf("%d B", bytes) + case bytes < MB: + return fmt.Sprintf("%.1f KB", float64(bytes)/KB) + case bytes < GB: + return fmt.Sprintf("%.1f MB", float64(bytes)/MB) + default: + return fmt.Sprintf("%.1f GB", float64(bytes)/GB) + } +} + func main() { if envUploadDir := os.Getenv("UPLOAD_DIR"); envUploadDir != "" { uploadDir = envUploadDir |
