From 9691fb7037458cfa24920ae9e07ec523887f1786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torjus=20H=C3=A5kestad?= Date: Sun, 22 Oct 2023 23:46:41 +0200 Subject: [PATCH] Initial commit --- Dockerfile | 14 ++++ cmd/main.go | 48 ++++++++++++++ go.mod | 23 +++++++ go.sum | 44 +++++++++++++ mf-proxy.toml | 7 ++ models.go | 11 ++++ server/config.go | 27 ++++++++ server/middleware.go | 35 ++++++++++ server/server.go | 148 +++++++++++++++++++++++++++++++++++++++++++ version.go | 3 + 10 files changed, 360 insertions(+) create mode 100644 Dockerfile create mode 100644 cmd/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 mf-proxy.toml create mode 100644 models.go create mode 100644 server/config.go create mode 100644 server/middleware.go create mode 100644 server/server.go create mode 100644 version.go diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ad3fede --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:alpine as build +RUN apk add --no-cache git +WORKDIR /app +COPY go.sum /app/go.sum +COPY go.mod /app/go.mod +ENV GOPRIVATE="git.t-juice.club" +RUN go mod download +COPY . /app +RUN go build -o mf-proxy cmd/main.go + +FROM golang:alpine +COPY --from=build /app/mf-proxy /usr/bin/mf-proxy +WORKDIR /app +CMD ["/usr/bin/mf-proxy"] \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..a0eecea --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "log/slog" + "os" + + "git.t-juice.club/microfilm/proxy/server" + "github.com/urfave/cli/v2" +) + +func main() { + logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, + })) + + app := &cli.App{ + Name: "mf-proxy", + Usage: "Run microfilm proxy service", + Action: func(c *cli.Context) error { + // Read config + f, err := os.Open("mf-proxy.toml") + if err != nil { + logger.Error("Error opening config.", "error", err) + os.Exit(1) + } + cfg, err := server.ConfigFromReader(f) + if err != nil { + logger.Error("Error parsing config.", "error", err) + os.Exit(1) + } + srv, err := server.NewServer(cfg) + if err != nil { + logger.Error("Error setting up server.", "error", err) + os.Exit(2) + } + + // Start server + srv.Logger = logger + logger.Info("Starting server.", "addr", srv.Addr) + return srv.ListenAndServe() + }, + } + + if err := app.Run(os.Args); err != nil { + logger.Error("Error running service.", "error", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b10b615 --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module git.t-juice.club/microfilm/proxy + +go 1.21.3 + +require ( + git.t-juice.club/microfilm/auth v0.1.1 + github.com/go-chi/chi/v5 v5.0.10 + github.com/nats-io/nats.go v1.31.0 + github.com/pelletier/go-toml/v2 v2.1.0 + github.com/urfave/cli/v2 v2.25.7 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/golang-jwt/jwt/v5 v5.0.0 // indirect + github.com/klauspost/compress v1.17.0 // indirect + github.com/nats-io/nkeys v0.4.5 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/crypto v0.6.0 // indirect + golang.org/x/sys v0.5.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1f2df98 --- /dev/null +++ b/go.sum @@ -0,0 +1,44 @@ +git.t-juice.club/microfilm/auth v0.1.1 h1:usg48CEd94Ha2rkEdCU+mhczJvLwwxVouOl478YdZFE= +git.t-juice.club/microfilm/auth v0.1.1/go.mod h1:sfgaIWxnNgERWyx611596OtEBc3cF4g3FSqKd073Te4= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E= +github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8= +github.com/nats-io/nkeys v0.4.5 h1:Zdz2BUlFm4fJlierwvGK+yl20IAKUm7eV6AAZXEhkPk= +github.com/nats-io/nkeys v0.4.5/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mf-proxy.toml b/mf-proxy.toml new file mode 100644 index 0000000..c556013 --- /dev/null +++ b/mf-proxy.toml @@ -0,0 +1,7 @@ +ListenAddr = ":8085" +NATSAddr = "nats:4222" +BaseSubject = "microfilm.proxy.v1" + +UserServiceBaseURL = "http://mf-users:8080" +AuthServiceBaseURL = "http://mf-auth:8082" +MovieServiceBaseURL = "http://mf-movies:8081" \ No newline at end of file diff --git a/models.go b/models.go new file mode 100644 index 0000000..c1acd02 --- /dev/null +++ b/models.go @@ -0,0 +1,11 @@ +package proxy + +// Request/response +type InfoResponse struct { + Version string `json:"version"` +} + +type ErrorResponse struct { + Status int `json:"status"` + Message string `json:"message"` +} diff --git a/server/config.go b/server/config.go new file mode 100644 index 0000000..e8237c0 --- /dev/null +++ b/server/config.go @@ -0,0 +1,27 @@ +package server + +import ( + "io" + + "github.com/pelletier/go-toml/v2" +) + +type Config struct { + ListenAddr string `toml:"ListenAddr"` + NATSAddr string `toml:"NATSAddr"` + BaseSubject string `toml:"BaseSubject"` + + UserServiceBaseURL string `toml:"UserServiceBaseURL"` + AuthServiceBaseURL string `toml:"AuthServiceBaseURL"` + MovieServiceBaseURL string `toml:"MovieServiceBaseURL"` +} + +func ConfigFromReader(r io.Reader) (*Config, error) { + decoder := toml.NewDecoder(r) + var c Config + if err := decoder.Decode(&c); err != nil { + return nil, err + } + + return &c, nil +} diff --git a/server/middleware.go b/server/middleware.go new file mode 100644 index 0000000..ffbc1ff --- /dev/null +++ b/server/middleware.go @@ -0,0 +1,35 @@ +package server + +import ( + "net/http" + "time" + + "github.com/go-chi/chi/v5/middleware" +) + +func (s *Server) MiddlewareLogging(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + + reqID := middleware.GetReqID(r.Context()) + + t1 := time.Now() + defer func() { + s.Logger.Info("Served request.", + "status", ww.Status(), + "method", r.Method, + "path", r.URL.Path, + "duration", time.Since(t1), + "remote", r.RemoteAddr, + "written", ww.BytesWritten(), + "req", reqID, + ) + s.Logger.Debug("Debug info.", + "req", reqID, + "headers", r.Header, + ) + }() + next.ServeHTTP(ww, r) + } + return http.HandlerFunc(fn) +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..19ed4a9 --- /dev/null +++ b/server/server.go @@ -0,0 +1,148 @@ +package server + +import ( + "encoding/json" + "io" + "log/slog" + "net/http" + "net/url" + "strings" + + "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" +) + +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.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 + + return srv, 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) { + client := http.Client{} + defer r.Body.Close() + + newURL := s.convertURL(r.URL, service) + req, err := http.NewRequest(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 +} diff --git a/version.go b/version.go new file mode 100644 index 0000000..afac7cb --- /dev/null +++ b/version.go @@ -0,0 +1,3 @@ +package proxy + +const Version = "v0.1.0"