package main import ( "encoding/json" "errors" "flag" "fmt" "log" "mime" "net" "net/http" "os" "path" "path/filepath" "strings" "time" ) /* ---------- Union-FS wie zuvor ---------- */ type unionFS struct{ roots []http.FileSystem } func (u unionFS) Open(name string) (http.File, error) { for _, fs := range u.roots { f, err := fs.Open(name) if err == nil { return f, nil } } return nil, os.ErrNotExist } /* ---------- File-Handler wie zuvor ---------- */ type fileHandler struct { fs http.FileSystem autoIndex bool cacheMaxAge time.Duration } func (h fileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { upath := r.URL.Path if !strings.HasPrefix(upath, "/") { upath = "/" + upath } upath = path.Clean(upath) f, err := h.fs.Open(upath) if err != nil { http.NotFound(w, r) return } defer f.Close() fi, err := f.Stat() if err != nil { http.NotFound(w, r) return } if fi.IsDir() { if !strings.HasSuffix(r.URL.Path, "/") { http.Redirect(w, r, r.URL.Path+"/", http.StatusMovedPermanently) return } indexPath := path.Join(upath, "index.html") if ff, err := h.fs.Open(indexPath); err == nil { defer ff.Close() info, _ := ff.Stat() h.serveFile(w, r, indexPath, ff, info) return } if h.autoIndex { h.serveDirList(w, r, f) return } http.NotFound(w, r) return } h.serveFile(w, r, upath, f, fi) } func (h fileHandler) serveFile(w http.ResponseWriter, r *http.Request, name string, f http.File, fi os.FileInfo) { ctype := mime.TypeByExtension(strings.ToLower(filepath.Ext(name))) if ctype == "" { ctype = "application/octet-stream" } w.Header().Set("Content-Type", ctype) if h.cacheMaxAge > 0 { w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(h.cacheMaxAge.Seconds()))) } http.ServeContent(w, r, strings.TrimPrefix(name, "/"), fi.ModTime(), f) } func (h fileHandler) serveDirList(w http.ResponseWriter, r *http.Request, d http.File) { entries, err := d.Readdir(-1) if err != nil { http.Error(w, "cannot read directory", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, "Index of %s

Index of %s

") } /* ---------- Logging-Middleware ---------- */ type logResponseWriter struct { http.ResponseWriter status int bytes int64 } func (lw *logResponseWriter) WriteHeader(code int) { lw.status = code lw.ResponseWriter.WriteHeader(code) } func (lw *logResponseWriter) Write(b []byte) (int, error) { n, err := lw.ResponseWriter.Write(b) lw.bytes += int64(n) return n, err } func clientIP(r *http.Request, trustProxy bool) string { if trustProxy { // X-Forwarded-For: "client, proxy1, proxy2" if xff := r.Header.Get("X-Forwarded-For"); xff != "" { parts := strings.Split(xff, ",") return strings.TrimSpace(parts[0]) } if xr := r.Header.Get("X-Real-IP"); xr != "" { return xr } } host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { return r.RemoteAddr } return host } type accessRecord struct { TS string `json:"ts"` Method string `json:"method"` Path string `json:"path"` Status int `json:"status"` Bytes int64 `json:"bytes"` Duration string `json:"duration"` IP string `json:"ip"` UA string `json:"ua"` } func loggingMiddleware(next http.Handler, trustProxy bool, logJSON bool) http.Handler { // Für Text-Logs Datum+Uhrzeit (UTC) im Prefix if logJSON { log.SetFlags(0) } else { log.SetFlags(log.Ldate | log.Ltime) } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() lw := &logResponseWriter{ResponseWriter: w, status: 200} next.ServeHTTP(lw, r) rec := accessRecord{ TS: time.Now().UTC().Format(time.RFC3339Nano), Method: r.Method, Path: r.URL.RequestURI(), Status: lw.status, Bytes: lw.bytes, Duration: time.Since(start).String(), IP: clientIP(r, trustProxy), UA: r.UserAgent(), } if logJSON { if b, err := json.Marshal(rec); err == nil { log.Print(string(b)) } else { log.Printf("log marshal error: %v", err) } } else { log.Printf(`%s %s status=%d bytes=%d dur=%s ip=%s ua="%s"`, rec.Method, rec.Path, rec.Status, rec.Bytes, rec.Duration, rec.IP, rec.UA) } }) } /* ---------- main() mit Flags ---------- */ func main() { var ( addr = flag.String("addr", ":8080", "listen address") archiveRoot = flag.String("archive", "/data/mirror/archive.ubuntu.com/ubuntu", "archive ubuntu root") securityRoot = flag.String("security", "/data/mirror/security.ubuntu.com/ubuntu", "security ubuntu root") oldReleases = flag.String("old", "/data/mirror/old-releases.ubuntu.com/ubuntu", "old-releases ubuntu root") msRoot = flag.String("ms", "/data/mirror/packages.microsoft.com/repos", "microsoft repos root") autoIndex = flag.Bool("autoindex", true, "enable directory listings") cacheSeconds = flag.Int("cache", 600, "Cache-Control max-age seconds (0 to disable)") trustProxy = flag.Bool("trust-proxy", true, "trust X-Forwarded-For / X-Real-IP") logJSON = flag.Bool("log-json", false, "log in JSON lines") ) flag.Parse() roots := []http.FileSystem{} for _, p := range []string{*archiveRoot, *securityRoot, *oldReleases} { if p == "" { continue } if st, err := os.Stat(p); err == nil && st.IsDir() { roots = append(roots, http.Dir(p)) log.Printf("added root: %s", p) } else { if errors.Is(err, os.ErrNotExist) { log.Printf("warn: root does not exist (skipping): %s", p) } else if err != nil { log.Printf("warn: cannot stat %s: %v", p, err) } } } if len(roots) == 0 { log.Fatal("no valid roots found") } union := unionFS{roots: roots} handler := fileHandler{ fs: union, autoIndex: *autoIndex, cacheMaxAge: time.Duration(*cacheSeconds) * time.Second, } mux := http.NewServeMux() mux.Handle("/ubuntu/", http.StripPrefix("/ubuntu", handler)) // Microsoft-Repos unter /ms/ (nur wenn vorhanden) if st, err := os.Stat(*msRoot); err == nil && st.IsDir() { mux.Handle("/ms/", http.StripPrefix("/ms", fileHandler{ fs: http.Dir(*msRoot), autoIndex: *autoIndex, cacheMaxAge: time.Duration(*cacheSeconds) * time.Second, })) log.Printf("serving Microsoft repos at /ms from %s", *msRoot) } else { log.Printf("warn: microsoft root not found (%s) — /ms disabled", *msRoot) } mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) wrapped := loggingMiddleware(mux, *trustProxy, *logJSON) srv := &http.Server{ Addr: *addr, Handler: wrapped, ReadTimeout: 30 * time.Second, WriteTimeout: 10 * time.Minute, IdleTimeout: 120 * time.Second, ReadHeaderTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, } log.Printf("listening on %s", *addr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err) } }