package gateway import ( "bufio" "fmt" "net" "net/http" "time" apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors" ) // cors sets the Access-Control-* headers for origins in the configured allow // list and short-circuits OPTIONS preflight requests. An empty origin (same-origin // or non-browser client) is passed through untouched. func (s *Server) cors(next http.Handler) http.Handler { allowed, allowAll := s.allowedOrigins() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if origin := r.Header.Get("Origin"); origin != "" && (allowAll || allowed[origin]) { if allowAll { w.Header().Set("Access-Control-Allow-Origin", "*") } else { w.Header().Set("Access-Control-Allow-Origin", origin) } w.Header().Set("Vary", "Origin") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") w.Header().Set("Access-Control-Max-Age", "600") } if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } // allowedOrigins resolves the configured origin list into a lookup set plus an // allow-all flag for the "*" wildcard. func (s *Server) allowedOrigins() (map[string]bool, bool) { list := s.cfg.AllowedOrigins if len(list) == 0 { return map[string]bool{}, false } allowAll := false out := make(map[string]bool, len(list)) for _, o := range list { if o == "*" { allowAll = true continue } out[o] = true } return out, allowAll } // recover converts a panic in a downstream handler into a 500 {error:...} // response instead of crashing the process, and logs the panic. func (s *Server) recover(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { s.log.Error("panic recovered", "panic", rec, "method", r.Method, "path", r.URL.Path) writeError(w, apperr.ErrInternal) } }() next.ServeHTTP(w, r) }) } // logging records one structured line per request (method, path, status, dur). func (s *Server) logging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rec, r) s.log.Info("request", "method", r.Method, "path", r.URL.Path, "status", rec.status, "duration_ms", time.Since(start).Milliseconds(), ) }) } // statusRecorder captures the written status so the logging middleware can report // it. It forwards the optional Hijacker/Flusher interfaces so the WebSocket // upgrader (which requires http.Hijacker) and streaming handlers still work. type statusRecorder struct { http.ResponseWriter status int } // WriteHeader records the status before delegating. func (r *statusRecorder) WriteHeader(code int) { r.status = code r.ResponseWriter.WriteHeader(code) } // Hijack forwards to the underlying writer when it supports connection hijacking. func (r *statusRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { if hj, ok := r.ResponseWriter.(http.Hijacker); ok { return hj.Hijack() } return nil, nil, fmt.Errorf("gateway: hijack unsupported by %T", r.ResponseWriter) } // Flush forwards to the underlying writer when it supports flushing. func (r *statusRecorder) Flush() { if f, ok := r.ResponseWriter.(http.Flusher); ok { f.Flush() } }