package httpserver import ( "compress/gzip" "crypto/rand" "encoding/base64" "html/template" "io" "io/fs" "mime" "net/http" "path" "path/filepath" "strings" "time" "go.uber.org/zap" "go.c3c.cz/cv/app/server/internal/files" "go.c3c.cz/cv/app/server/internal/version" ) type handler struct { indexTemplate *template.Template logger *zap.Logger frontendConfig string } func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { f, err := files.Public.Open(path.Join("data", "public", r.URL.Path)) if err != nil { h.serveIndex(w) return } fi, err := f.Stat() if err != nil || fi.IsDir() { h.serveIndex(w) return } h.serveFile(w, r, version.CommitTime, f) } func (h *handler) serveFile(w http.ResponseWriter, r *http.Request, modtime time.Time, f fs.File) { if !modtime.IsZero() { ims := r.Header.Get("If-Modified-Since") modtime = modtime.Truncate(time.Second) if t, err := http.ParseTime(ims); err == nil && !modtime.After(t) { w.WriteHeader(http.StatusNotModified) return } w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat)) } w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(r.URL.Path))) if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { w.Header().Set("Content-Encoding", "gzip") gw, err := gzip.NewWriterLevel(w, gzip.DefaultCompression) if err != nil { h.logger.Warn("Could not create gzip writer", zap.Error(err)) return } if n, _ := io.Copy(gw, f); n == 0 { w.Header().Del("Content-Encoding") gw.Reset(io.Discard) } err = gw.Close() if err != nil { h.logger.Warn("Could not close gzip writer", zap.Error(err)) return } } else { _, err := io.Copy(w, f) if err != nil { h.logger.Warn("Could not write file response", zap.Error(err)) return } } } type indexData struct { Config string CspNonce string } func (h *handler) serveIndex(w http.ResponseWriter) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if h.indexTemplate == nil { _, _ = w.Write([]byte("no index")) return } token := make([]byte, 12) _, err := rand.Read(token) if err != nil { h.logger.Warn("Could not create random csp token", zap.Error(err)) return } data := indexData{ Config: h.frontendConfig, CspNonce: base64.RawStdEncoding.EncodeToString(token), } w.Header().Set("Content-Security-Policy", "script-src 'self' 'nonce-"+data.CspNonce+"' 'unsafe-eval'") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-XSS-Protection", "1; mode=block") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Referrer-Policy", "same-origin") w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") w.Header().Set("Permissions-Policy", "accelerometer=(), camera=(), geolocation=(), microphone=(), payment=(), usb=()") err = h.indexTemplate.Execute(w, data) if err != nil { h.logger.Warn("Could not execute index template", zap.Error(err)) } }