Skip to content

Commit

Permalink
update proxy for new m&m api
Browse files Browse the repository at this point in the history
  • Loading branch information
its-felix committed Dec 13, 2024
1 parent e204074 commit ee7e52a
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 79 deletions.
9 changes: 9 additions & 0 deletions go/proxy/config_local.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build !prod

package main

import "log/slog"

func init() {
slog.SetLogLoggerLevel(slog.LevelDebug)
}
5 changes: 0 additions & 5 deletions go/proxy/consts_local.go

This file was deleted.

5 changes: 0 additions & 5 deletions go/proxy/consts_prod.go

This file was deleted.

13 changes: 13 additions & 0 deletions go/proxy/headers_local.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//go:build !prod

package main

import "net/http"

func addAccessControlHeaders(h http.Header) {
h.Set("Access-Control-Allow-Origin", "*")
h.Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
h.Set("Access-Control-Allow-Headers", "*")
h.Set("Access-Control-Allow-Credentials", "true")
h.Set("Access-Control-Max-Age", "86400")
}
13 changes: 13 additions & 0 deletions go/proxy/headers_prod.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//go:build prod

package main

import "net/http"

func addAccessControlHeaders(h http.Header) {
h.Set("Access-Control-Allow-Origin", "*")
h.Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
h.Set("Access-Control-Allow-Headers", "*")
h.Set("Access-Control-Allow-Credentials", "true")
h.Set("Access-Control-Max-Age", "86400")
}
91 changes: 22 additions & 69 deletions go/proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,23 @@ package main
import (
"context"
"errors"
"io"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
)

func addAccessControlHeaders(h http.Header) {
h.Set("Access-Control-Allow-Origin", AllowOrigin)
h.Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
h.Set("Access-Control-Allow-Headers", "*")
h.Set("Access-Control-Allow-Credentials", "true")
h.Set("Access-Control-Max-Age", "86400")
}

func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
defer stop()

p, err := NewProxy("/milesandmore")
if err != nil {
panic(err)
}

mux := http.NewServeMux()
mux.HandleFunc("OPTIONS /", func(w http.ResponseWriter, req *http.Request) {
addAccessControlHeaders(w.Header())
Expand All @@ -38,78 +33,36 @@ func main() {
_, _ = w.Write([]byte("github.com/explore-flights/monorepo/go/proxy"))
})

mux.HandleFunc("POST /milesandmore/", func(w http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), time.Second*15)
defer cancel()

if req.Body != nil {
defer req.Body.Close()
}

outurl := "https://api.miles-and-more.com"
outurl += strings.TrimPrefix(req.URL.EscapedPath(), "/milesandmore")
if req.URL.RawQuery != "" {
outurl += "?"
outurl += req.URL.RawQuery
}

outreq, err := http.NewRequestWithContext(ctx, req.Method, outurl, req.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
}

for k, v := range req.Header {
if strings.EqualFold(k, "user-agent") || strings.EqualFold(k, "accept") || strings.EqualFold(k, "x-api-key") {
outreq.Header[k] = v
}
}

outreq.Header.Set("Origin", "https://www.miles-and-more.com")
outreq.Header.Set("Referer", "https://www.miles-and-more.com/")

proxyresp, err := http.DefaultClient.Do(outreq)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
w.WriteHeader(http.StatusRequestTimeout)
} else {
w.WriteHeader(http.StatusBadGateway)
}
mux.Handle("POST /milesandmore/", p)

_, _ = w.Write([]byte(err.Error()))
return
}

defer proxyresp.Body.Close()

if contentType := proxyresp.Header.Get("Content-Type"); contentType != "" {
w.Header().Set("Content-Type", contentType)
}

addAccessControlHeaders(w.Header())

w.WriteHeader(proxyresp.StatusCode)
_, _ = io.Copy(w, proxyresp.Body)
})

if err := run(ctx, &http.Server{Addr: "127.0.0.1:8090", Handler: mux}); err != nil {
if err = run(ctx, mux); err != nil {
panic(err)
}

slog.Info("proxy stopped")
}

func run(ctx context.Context, srv *http.Server) error {
func run(ctx context.Context, handler http.Handler) error {
const addr = "127.0.0.1:8090"

ctx, cancel := context.WithCancel(ctx)
defer cancel()

l, err := net.Listen("tcp", addr)
if err != nil {
return err
}

slog.Info("proxy ready to accept connections", slog.String("addr", addr))

go func() {
<-ctx.Done()
if err := srv.Shutdown(context.Background()); err != nil {
if err := l.Close(); err != nil {
slog.Error("error shutting down the http server", slog.String("err", err.Error()))
}
}()

if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
if err := http.Serve(l, handler); err != nil && !errors.Is(err, net.ErrClosed) {
return err
}

Expand Down
182 changes: 182 additions & 0 deletions go/proxy/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package main

import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/cookiejar"
"strings"
"sync"
"sync/atomic"
"time"
)

type Proxy struct {
prefix string
httpClient *http.Client
init *atomic.Bool
mtx *sync.Mutex
}

func NewProxy(prefix string) (*Proxy, error) {
jar, err := cookiejar.New(nil)
if err != nil {
return nil, err
}

httpClient := &http.Client{
Jar: jar,
}

return &Proxy{
prefix: prefix,
httpClient: httpClient,
init: new(atomic.Bool),
mtx: new(sync.Mutex),
}, nil
}

func (p *Proxy) ensureInit(ctx context.Context, inReq *http.Request) error {
if p.init.Load() {
return nil
}

req, err := p.prepareRequest(ctx, inReq, http.MethodGet, "https://www.miles-and-more.com/de/de/spend/flights/flight-award.html", nil)
if err != nil {
return err
}

resp, err := p.doRequest(req)
if err != nil {
return err
}

_ = resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}

p.init.Store(true)
slog.Info("proxy initialized")
return nil
}

func (p *Proxy) prepareRequest(ctx context.Context, inReq *http.Request, method, url string, body io.Reader) (*http.Request, error) {
outReq, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, err
}

if inReq != nil {
proxyHeaders := []string{
"user-agent",
"accept",
"accept-encoding",
"accept-language",
"content-type",
"content-length",
"cache-control",
"pragma",
"x-api-key",
"rtw",
}

for _, h := range proxyHeaders {
value := inReq.Header.Get(h)
if value != "" {
outReq.Header.Set(h, value)
}
}

outReq.URL.RawQuery = inReq.URL.RawQuery
}

outReq.Header.Set("Origin", "https://www.miles-and-more.com")
outReq.Header.Set("Referer", "https://www.miles-and-more.com/")

if outReq.Header.Get("User-Agent") == "" {
outReq.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36")
}

return outReq, nil
}

func (p *Proxy) doRequest(r *http.Request) (*http.Response, error) {
slog.Debug(
"sending proxy request",
slog.String("method", r.Method),
slog.String("url", r.URL.String()),
slog.Any("headers", r.Header),
)

resp, err := p.httpClient.Do(r)
if err == nil {
slog.Debug("proxy request succeeded", slog.Int("status", resp.StatusCode))
} else {
slog.Debug("proxy request failed", slog.String("err", err.Error()))
}

return resp, err
}

func (p *Proxy) ServeHTTP(w http.ResponseWriter, inReq *http.Request) {
p.mtx.Lock()
defer p.mtx.Unlock()

ctx, cancel := context.WithTimeout(inReq.Context(), time.Second*15)
defer cancel()

err := p.ensureInit(ctx, inReq)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
}

outUrl := "https://api.miles-and-more.com"
outUrl += strings.TrimPrefix(inReq.URL.EscapedPath(), p.prefix)

outReq, err := p.prepareRequest(ctx, inReq, inReq.Method, outUrl, inReq.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
}

proxyResp, err := p.doRequest(outReq)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
w.WriteHeader(http.StatusRequestTimeout)
} else {
w.WriteHeader(http.StatusBadGateway)
}

_, _ = w.Write([]byte(err.Error()))
return
}

defer proxyResp.Body.Close()

proxyHeaders := []string{
"content-type",
"content-length",
"content-encoding",
"date",
}

for _, h := range proxyHeaders {
value := proxyResp.Header.Get(h)
if value != "" {
w.Header().Set(h, value)
}
}

addAccessControlHeaders(w.Header())

w.WriteHeader(proxyResp.StatusCode)
_, _ = io.Copy(w, proxyResp.Body)
}

0 comments on commit ee7e52a

Please sign in to comment.