update mit logging
All checks were successful
release-tag / release-image (push) Successful in 1m27s

This commit is contained in:
2025-08-29 23:27:53 +02:00
parent e86bd571ee
commit 176fe99ac0
2 changed files with 118 additions and 46 deletions

View File

@@ -1,20 +1,21 @@
version: "3.9"
services:
updater:
image: git.send.nrw/sendnrw/go-ubuntu-mirror:latest # unverändert vom vorherigen Setup
container_name: ubuntu-mirror-updater
restart: unless-stopped
volumes:
- mirror-data:/var/spool/apt-mirror
- /docker/ubuntu-apt-mirror/data:/var/spool/apt-mirror
networks:
- traefik-net
image: compose-updater
web:
build:
context: .
dockerfile: Dockerfile # <- die Alpine-Datei oben
image: git.send.nrw/sendnrw/go-ubuntu-mirror:latest
container_name: ubuntu-mirror-web
restart: unless-stopped
depends_on: [updater]
depends_on:
- updater
volumes:
- mirror-data:/data:ro
- /docker/ubuntu-apt-mirror/data:/data
command: >
-archive=/data/mirror/archive.ubuntu.com/ubuntu
-security=/data/mirror/security.ubuntu.com/ubuntu
@@ -22,31 +23,24 @@ services:
-autoindex=true
-cache=600
-addr=:8080
-trust-proxy=true
-log-json=false # auf true setzen für JSON-Lines
labels:
- traefik.enable=true
# HTTPS Router
- traefik.http.routers.ubuntu_mirror.rule=Host(`ubuntu-24-04.send.nrw`)
- traefik.http.routers.ubuntu_mirror.entrypoints=websecure
- traefik.http.routers.ubuntu_mirror.tls=true
- traefik.http.routers.ubuntu_mirror.tls.certresolver=letsencrypt
- traefik.http.routers.ubuntu_mirror.service=ubuntu_mirror_svc
# HTTP -> HTTPS Redirect
- traefik.http.services.ubuntu_mirror_svc.loadbalancer.server.port=8080
- traefik.http.services.ubuntu_mirror_svc.loadbalancer.server.scheme=http
- traefik.http.routers.ubuntu_mirror_http.rule=Host(`ubuntu-24-04.send.nrw`)
- traefik.http.routers.ubuntu_mirror_http.entrypoints=web
- traefik.http.routers.ubuntu_mirror_http.middlewares=to-https
- traefik.http.routers.ubuntu_mirror_http.service=ubuntu_mirror_svc
- traefik.http.middlewares.to-https.redirectscheme.scheme=https
- traefik.http.middlewares.to-https.redirectscheme.permanent=true
# Service-Ziel (Go-Server auf 8080)
- traefik.http.services.ubuntu_mirror_svc.loadbalancer.server.port=8080
- traefik.http.services.ubuntu_mirror_svc.loadbalancer.server.scheme=http
# Optional: externes Traefik-Netz
# - traefik.docker.network=traefik_proxy
# networks:
# - traefik_proxy
volumes:
mirror-data:
# networks:
# traefik_proxy:
# external: true
networks:
- traefik-net
networks:
traefik-net:
external: true

122
main.go
View File

@@ -1,11 +1,13 @@
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"mime"
"net"
"net/http"
"os"
"path"
@@ -14,7 +16,8 @@ import (
"time"
)
// unionFS tries multiple http.FileSystem roots in order.
/* ---------- Union-FS wie zuvor ---------- */
type unionFS struct{ roots []http.FileSystem }
func (u unionFS) Open(name string) (http.File, error) {
@@ -27,7 +30,8 @@ func (u unionFS) Open(name string) (http.File, error) {
return nil, os.ErrNotExist
}
// fileHandler serves files from an http.FileSystem with nice directory listing
/* ---------- File-Handler wie zuvor ---------- */
type fileHandler struct {
fs http.FileSystem
autoIndex bool
@@ -35,14 +39,12 @@ type fileHandler struct {
}
func (h fileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Normalize path
upath := r.URL.Path
if !strings.HasPrefix(upath, "/") {
upath = "/" + upath
}
upath = path.Clean(upath) // prevents path traversal
upath = path.Clean(upath)
// Open
f, err := h.fs.Open(upath)
if err != nil {
http.NotFound(w, r)
@@ -56,14 +58,11 @@ func (h fileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// Directories
if fi.IsDir() {
// redirect to slash-terminated path (as net/http does)
if !strings.HasSuffix(r.URL.Path, "/") {
http.Redirect(w, r, r.URL.Path+"/", http.StatusMovedPermanently)
return
}
// If index.html exists, serve it
indexPath := path.Join(upath, "index.html")
if ff, err := h.fs.Open(indexPath); err == nil {
defer ff.Close()
@@ -71,7 +70,6 @@ func (h fileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.serveFile(w, r, indexPath, ff, info)
return
}
// Otherwise: simple autoindex (for debugging in browser)
if h.autoIndex {
h.serveDirList(w, r, f)
return
@@ -80,25 +78,18 @@ func (h fileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// Files
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) {
// Content-Type by extension (fallback: octet-stream)
ctype := mime.TypeByExtension(strings.ToLower(filepath.Ext(name)))
if ctype == "" {
ctype = "application/octet-stream"
}
w.Header().Set("Content-Type", ctype)
// Conservative cache (APT macht eigene Validierungen über InRelease/Release)
if h.cacheMaxAge > 0 {
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(h.cacheMaxAge.Seconds())))
}
// ServeContent enables Range requests + sets Last-Modified
// Use name without leading slash to avoid special-case in ServeContent
http.ServeContent(w, r, strings.TrimPrefix(name, "/"), fi.ModTime(), f)
}
@@ -128,15 +119,102 @@ func (h fileHandler) serveDirList(w http.ResponseWriter, r *http.Request, d http
fmt.Fprint(w, "</ul>")
}
/* ---------- 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")
// Point these to the *ubuntu* directories inside your mirror volume
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")
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()
@@ -168,19 +246,19 @@ func main() {
}
mux := http.NewServeMux()
// Serve under /ubuntu/... to match client sources.list
mux.Handle("/ubuntu/", http.StripPrefix("/ubuntu", handler))
// health endpoint
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: mux,
Handler: wrapped,
ReadTimeout: 30 * time.Second,
WriteTimeout: 10 * time.Minute, // big files
WriteTimeout: 10 * time.Minute,
IdleTimeout: 120 * time.Second,
ReadHeaderTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,