package server import ( "context" "encoding/json" "io" "log/slog" "net/http" "net/url" "strings" "time" "git.t-juice.club/microfilm/auth" "git.t-juice.club/microfilm/auth/authmw" "git.t-juice.club/microfilm/proxy" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/nats-io/nats.go" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" "go.opentelemetry.io/otel/trace" ) type Service string const ( ServiceUser Service = "user" ServiceAuth Service = "auth" ServiceMovies Service = "movies" ) type Server struct { Logger *slog.Logger config *Config nats *nats.EncodedConn http.Server } func NewServer(config *Config) (*Server, error) { srv := &Server{} srv.config = config r := chi.NewRouter() r.Use(middleware.RealIP) r.Use(middleware.RequestID) r.Use(srv.MiddlewareTracing) r.Use(srv.MiddlewareLogging) r.Get("/info", InfoHandler) r.Handle("/user/*", srv.ProxyHandler(ServiceUser)) r.Handle("/auth/*", srv.ProxyHandler(ServiceAuth)) r.With(authmw.VerifyToken("http://mf-auth:8082", []string{auth.RoleAdmin})).Get("/testauth/admin", InfoHandler) srv.Handler = r srv.Addr = config.ListenAddr tp, err := tracerProvider("jaeger:4318") if err != nil { return nil, err } otel.SetTracerProvider(tp) return srv, nil } func tracerProvider(url string) (*sdktrace.TracerProvider, error) { exp, err := otlptracehttp.New(context.Background(), otlptracehttp.WithEndpoint(url), otlptracehttp.WithInsecure()) if err != nil { return nil, err } res := resource.NewWithAttributes(semconv.SchemaURL, semconv.ServiceName("mf-proxy"), semconv.ServiceVersion(proxy.Version), ) tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exp, sdktrace.WithBatchTimeout(time.Second)), sdktrace.WithResource(res), ) otel.SetTracerProvider(tp) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) return tp, nil } func InfoHandler(w http.ResponseWriter, r *http.Request) { enc := json.NewEncoder(w) data := &proxy.InfoResponse{ Version: proxy.Version, } _ = enc.Encode(data) } func WriteError(w http.ResponseWriter, response proxy.ErrorResponse) { w.WriteHeader(response.Status) encoder := json.NewEncoder(w) _ = encoder.Encode(&response) } func (s *Server) ProxyHandler(service Service) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() client := http.Client{ Transport: otelhttp.NewTransport(http.DefaultTransport), } defer r.Body.Close() newURL := s.convertURL(r.URL, service) span := trace.SpanFromContext(ctx) span.AddEvent("Do proxy forward") req, err := http.NewRequestWithContext(ctx, r.Method, newURL.String(), r.Body) if err != nil { s.Logger.Warn("Failed to create forwarding request.", "error", err, "url", newURL) WriteError(w, proxy.ErrorResponse{ Status: http.StatusInternalServerError, Message: "Failed to create request.", }) return } for k, h := range r.Header { for _, v := range h { req.Header.Add(k, v) } } req.Header.Add("X-Forwarded-For", r.RemoteAddr) reqID := middleware.GetReqID(r.Context()) req.Header.Add("X-Request-Id", reqID) resp, err := client.Do(req) if err != nil { s.Logger.Warn("Failed to perform forwarding request.", "error", err, "url", newURL) WriteError(w, proxy.ErrorResponse{ Status: http.StatusInternalServerError, Message: "Failed to create request.", }) } defer resp.Body.Close() for key, values := range resp.Header { for _, value := range values { w.Header().Add(key, value) } } w.WriteHeader(resp.StatusCode) _, _ = io.Copy(w, resp.Body) s.Logger.Debug("Performed proxy request.", "new_url", newURL, "original_url", r.URL.String(), "reqID", reqID) } return http.HandlerFunc(fn) } func (s *Server) convertURL(requestURL *url.URL, service Service) *url.URL { oldURL := *requestURL var targetURL *url.URL var err error var stripPrefix string switch service { case ServiceAuth: targetURL, err = url.Parse(s.config.AuthServiceBaseURL) if err != nil { panic(err) } stripPrefix = "/auth" case ServiceUser: targetURL, err = url.Parse(s.config.UserServiceBaseURL) if err != nil { panic(err) } stripPrefix = "/user" } oldURL.Scheme = targetURL.Scheme oldURL.Host = targetURL.Host oldURL.Path, _ = strings.CutPrefix(oldURL.Path, stripPrefix) return &oldURL }