package main import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "errors" "fmt" "image" "image/draw" "image/jpeg" "image/png" "io" "io/fs" "log" "net" "net/http" "os" "path/filepath" "strings" "sync" "time" "github.com/auyer/steganography" "golang.org/x/image/webp" "gopkg.in/yaml.v3" ) type Config struct { Server struct { Port string `yaml:"port"` ImageDir string `yaml:"image_dir"` BasePath string `yaml:"base_path"` CacheImages bool `yaml:"cache_images"` } `yaml:"server"` Security struct { AESKey string `yaml:"aes_key"` // Base64 encoded 32-byte key } `yaml:"security"` } func loadConfig(filename string) error { data, err := os.ReadFile(filename) if err != nil { return fmt.Errorf("config file read error: %w", err) } if err := yaml.Unmarshal(data, &cfg); err != nil { return fmt.Errorf("config parse error: %w", err) } // Set defaults if not specified if cfg.Server.Port == "" { cfg.Server.Port = "8219" } if cfg.Server.ImageDir == "" { cfg.Server.ImageDir = "./images" } if cfg.Server.BasePath == "" { cfg.Server.BasePath = "/" } return nil } var ( decodedImageCache sync.Map // path -> *cachedImage aesKey []byte cfg Config ) type cachedImage struct { img *image.NRGBA mimeType string maxPayloadSize uint32 size uint32 } func init() { if err := loadConfig("config.yaml"); err != nil { log.Fatalf("Failed to load config: %v", err) } if cfg.Security.AESKey != "" { var err error aesKey, err = base64.StdEncoding.DecodeString(cfg.Security.AESKey) if err != nil { log.Fatalf("Invalid AES key in config: %v", err) } } else { aesKey = make([]byte, 32) if _, err := rand.Read(aesKey); err != nil { log.Fatalf("Failed to generate AES key: %v", err) } log.Printf("Generated new AES key (base64): %s", base64.StdEncoding.EncodeToString(aesKey)) } if cfg.Server.CacheImages { if err := loadImages(); err != nil { log.Printf("Warning: could not preload images: %v", err) } } } func loadImages() error { return filepath.WalkDir(cfg.Server.ImageDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } ext := strings.ToLower(filepath.Ext(path)) switch ext { case ".jpg", ".jpeg", ".png", ".webp": data, err := os.ReadFile(path) size := uint32(len(data)) if err != nil { return err } // Preload decoded image img, mimeType, err := decodeImageData(data, ext) if err != nil { return fmt.Errorf("error decoding %s: %v", path, err) } nrgba := image.NewNRGBA(img.Bounds()) draw.Draw(nrgba, nrgba.Bounds(), img, img.Bounds().Min, draw.Src) maxSize := steganography.MaxEncodeSize(nrgba) maxPlaintext, err := maxPlaintextSize(maxSize) if err != nil { log.Printf("Skipping %s: %v", path, err) return nil } decodedImageCache.Store(path, &cachedImage{ img: nrgba, mimeType: mimeType, maxPayloadSize: maxPlaintext, size: size, }) log.Printf("Cached decoded image: %s", path) } return nil }) } func decodeImageData(data []byte, ext string) (image.Image, string, error) { switch ext { case ".jpg", ".jpeg": img, err := jpeg.Decode(bytes.NewReader(data)) return img, "image/jpeg", err case ".png": img, err := png.Decode(bytes.NewReader(data)) return img, "image/png", err case ".webp": img, err := webp.Decode(bytes.NewReader(data)) return img, "image/webp", err default: return nil, "", fmt.Errorf("unsupported format: %s", ext) } } func encryptData(plaintext []byte) ([]byte, error) { block, err := aes.NewCipher(aesKey) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } nonce := make([]byte, gcm.NonceSize()) if _, err = io.ReadFull(rand.Reader, nonce); err != nil { return nil, err } return gcm.Seal(nonce, nonce, plaintext, nil), nil } func maxPlaintextSize(totalOutputSize uint32) (uint32, error) { block, err := aes.NewCipher(aesKey) if err != nil { return 0, err } gcm, err := cipher.NewGCM(block) if err != nil { return 0, err } overhead := uint32(gcm.NonceSize() + gcm.Overhead()) if totalOutputSize < overhead { return 0, fmt.Errorf("output size too small") } return totalOutputSize - overhead, nil } var bufferPool = sync.Pool{ New: func() interface{} { return new(bytes.Buffer) }, } func embedData(img *image.NRGBA, data []byte) ([]byte, error) { buf := bufferPool.Get().(*bytes.Buffer) defer func() { buf.Reset() if buf.Cap() <= 2<<20 { // 2MB cap bufferPool.Put(buf) } }() err := steganography.Encode(buf, img, data) if err != nil { return nil, fmt.Errorf("failed to encode data into image: %w", err) } result := make([]byte, buf.Len()) copy(result, buf.Bytes()) return result, nil } func getClientInfo(r *http.Request) string { ip := getClientIP(r) var headers strings.Builder for name, values := range r.Header { for _, value := range values { headers.WriteString(fmt.Sprintf("%s: %s\n", name, value)) } } return fmt.Sprintf("%s\n%s\n%s", time.Now().UTC().Format(time.RFC3339), ip, headers.String()) } func getClientIP(r *http.Request) string { headers := []string{"X-Forwarded-For"} for _, h := range headers { if ip := r.Header.Get(h); ip != "" { parts := strings.Split(ip, ",") clientIP := strings.TrimSpace(parts[0]) if len(parts) > 1 && isTrustedProxy(clientIP) { return strings.TrimSpace(parts[1]) } return clientIP } } ip, _, _ := net.SplitHostPort(r.RemoteAddr) return ip } func isTrustedProxy(ip string) bool { return true // Implement actual IP validation if needed } func imageHandler(w http.ResponseWriter, r *http.Request) { relativePath := strings.TrimPrefix(r.URL.Path, cfg.Server.BasePath) requestedPath := filepath.Clean(strings.TrimPrefix(relativePath, "/")) imagePath := filepath.Join(cfg.Server.ImageDir, requestedPath) log.Printf("Requested image path: %s", imagePath) if !strings.HasPrefix(imagePath, filepath.Clean(cfg.Server.ImageDir)+string(os.PathSeparator)) { http.Error(w, "Invalid path", http.StatusBadRequest) return } if _, err := os.Stat(imagePath); errors.Is(err, os.ErrNotExist) { http.NotFound(w, r) return } var ci *cachedImage if cached, ok := decodedImageCache.Load(imagePath); ok { ci = cached.(*cachedImage) } else { data, err := os.ReadFile(imagePath) if err != nil { http.Error(w, "File read error", http.StatusInternalServerError) return } ext := strings.ToLower(filepath.Ext(imagePath)) img, mimeType, err := decodeImageData(data, ext) if err != nil { http.ServeFile(w, r, imagePath) return } nrgba := image.NewNRGBA(img.Bounds()) draw.Draw(nrgba, nrgba.Bounds(), img, img.Bounds().Min, draw.Src) maxSize := steganography.MaxEncodeSize(nrgba) maxPlaintext, err := maxPlaintextSize(maxSize) if err != nil { http.ServeFile(w, r, imagePath) return } ci = &cachedImage{ img: nrgba, mimeType: mimeType, maxPayloadSize: maxPlaintext, size: uint32(len(data)), } decodedImageCache.Store(imagePath, ci) } metadata := getClientInfo(r) if len(metadata) > int(ci.maxPayloadSize) { metadata = metadata[:ci.maxPayloadSize] } encryptedData, err := encryptData([]byte(metadata)) if err != nil { http.ServeFile(w, r, imagePath) return } modifiedImage, err := embedData(ci.img, encryptedData) if err != nil { http.ServeFile(w, r, imagePath) return } w.Header().Set("Content-Type", "image/png") w.Header().Set("Content-Length", fmt.Sprint(len(modifiedImage))) w.Write(modifiedImage) } func main() { if _, err := os.Stat(cfg.Server.ImageDir); os.IsNotExist(err) { log.Fatalf("Image directory does not exist: %s", cfg.Server.ImageDir) } basePath := strings.TrimSuffix(cfg.Server.BasePath, "/") http.HandleFunc(basePath+"/", imageHandler) log.Printf("Starting server on :%s", cfg.Server.Port) log.Fatal(http.ListenAndServe(":"+cfg.Server.Port, nil)) }