From 98876106ed01889c01b225c100c1f29c5da4a360 Mon Sep 17 00:00:00 2001 From: Elia el Lazkani Date: Tue, 11 Jul 2023 03:10:24 +0200 Subject: [PATCH] In the beginning the was darkness and then there was a reverse proxy. --- .drone.yml | 86 +++++++++++++++++++++++++++++++++++++++++++++++++ .gitignore | 2 ++ README.md | 4 +++ env.go | 12 +++++++ go.mod | 5 +++ main.go | 21 ++++++++++++ proxy.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ server.sh | 1 + token.go | 29 +++++++++++++++++ 9 files changed, 254 insertions(+) create mode 100644 .drone.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 env.go create mode 100644 go.mod create mode 100644 main.go create mode 100644 proxy.go create mode 100644 server.sh create mode 100644 token.go diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..a9af054 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,86 @@ +--- +kind: pipeline +name: test + +steps: +- name: test-code + image: golang + commands: + - go get + - go test + +--- +kind: pipeline +name: test-build + +steps: +- name: prepare + image: golang + commands: + - go version + +- name: build-linux-amd64 + image: golang + environment: + GOOS: linux + GOARCH: amd64 + commands: + - go get + - go build -o sidoxy-linux-amd64-${DRONE_COMMIT_SHA:0:8} + depends_on: + - prepare + +depends_on: + - test + +--- +kind: pipeline +name: build + +steps: +- name: prepare + image: golang + commands: + - mkdir bin/ + +- name: build-linux-amd64 + image: golang + environment: + GOOS: linux + GOARCH: amd64 + commands: + - go get + - go build -o bin/sidoxy-linux-amd64-${DRONE_TAG} + depends_on: + - prepare + +- name: generate-checksum + image: golang + commands: + - cd bin + - md5sum * > ../md5sums.txt + - sha512sum * > ../sha512sums.txt + - cp ../md5sums.txt . + - cp ../sha512sums.txt . + depends_on: + - build-linux-amd64 + +- name: gitea_release + image: plugins/gitea-release + settings: + title: Release ${DRONE_TAG} + note: This is the cmw release of version ${DRONE_TAG} + api_key: + from_secret: + gitea_release + base_url: https://scm.project42.io + files: bin/* + depends_on: + - generate-checksum + +depends_on: + - test-build + +trigger: + event: + - tag diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d76d7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.envrc +go.sum diff --git a/README.md b/README.md new file mode 100644 index 0000000..63435e8 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +Sidoxy +====== + +`Sidoxy` is a simple proxy written in [Go](https://go.dev/). diff --git a/env.go b/env.go new file mode 100644 index 0000000..c4227a6 --- /dev/null +++ b/env.go @@ -0,0 +1,12 @@ +package main + +import ( + "os" +) + +func getEnvironmentVariables() (proxyHostURL, proxyServerURL, jwtKey string) { + proxyHostURL = os.Getenv("PROXY_HOST_URL") + proxyServerURL = os.Getenv("PROXY_SERVER_URL") + jwtKey = os.Getenv("PROXY_JWT_KEY") + return +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..da9d306 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module sidoxy + +go 1.20 + +require github.com/golang-jwt/jwt/v5 v5.0.0 diff --git a/main.go b/main.go new file mode 100644 index 0000000..d3de46f --- /dev/null +++ b/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "log" +) + +func main() { + + proxyHostURL, proxyServerURL, jwtKey := getEnvironmentVariables() + + // define origin server URL + originServerURL := parse_url(proxyServerURL) + reverseProxy := &ReverseProxy{URL: originServerURL, jwtKey: jwtKey} + + fmt.Printf("[sidoxy] Listening on \"%s\"\n", proxyHostURL) + fmt.Printf("[sidoxy] Forwarding to \"%s\"\n", proxyServerURL) + fmt.Printf("[sidoxy] jwtKey is \"%s\"\n", jwtKey) + log.Fatal(listen_and_serve(proxyHostURL, reverseProxy.reverseProxyHandlerFunc())) + +} diff --git a/proxy.go b/proxy.go new file mode 100644 index 0000000..ab0bf59 --- /dev/null +++ b/proxy.go @@ -0,0 +1,94 @@ +package main + +import ( + "fmt" + "io" + "net" + "log" + "net/http" + "net/url" + "time" +) + +type ReverseProxy struct { + URL *url.URL + jwtKey string +} + +func (rp *ReverseProxy) reverseProxyHandlerFunc() http.Handler { + return http.HandlerFunc(rp.reverseProxyHandler) +} + +func (rp *ReverseProxy) reverseProxyHandler(rw http.ResponseWriter, req *http.Request) { + fmt.Printf("[sidoxy] Reverse proxy received request at: %s\n", time.Now()) + + // set req Host, URL and Request URI to forward a request to the origin server + req.Host = rp.URL.Host + req.URL.Host = rp.URL.Host + req.URL.Scheme = rp.URL.Scheme + req.RequestURI = "" + + // set X-FORWARDED-FOR + xForwardedFor := req.Header.Get("X-Forwarded-For") + var remoteAddr string + if xForwardedFor != "" { + remoteAddr = xForwardedFor + } else { + remoteAddr, _, _ = net.SplitHostPort(req.RemoteAddr) + } + req.Header.Set("X-Forwarded-For", remoteAddr) + fmt.Printf("[sidoxy] Reverse proxy setting X-Forwarded-For to %s\n", remoteAddr) + + // TODO: To be implemented + // decode JWT if key present + if rp.jwtKey != "" { + jwtDecode(rp.jwtKey, req.Header.Get("JWT_TOKEN"), req) + } + + // save the response from the origin server + originServerResponse, err := http.DefaultClient.Do(req) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + _, _ = fmt.Fprint(rw, err) + return + } + + fmt.Printf("[sidoxy] Reverse proxy setting the following return headers\n") + // return Header response + for key, values := range originServerResponse.Header { + for _, value := range values { + fmt.Printf("[sidoxy] \t%v: %v\n", key, value) + rw.Header().Set(key, value) + } + } + + // support stream + done := make(chan bool) + defer close(done) + go func() { + for { + select { + case <-time.Tick(10 * time.Millisecond): + rw.(http.Flusher).Flush() + case <-done: + return + } + } + }() + + // return response to the client + rw.WriteHeader(http.StatusOK) + io.Copy(rw, originServerResponse.Body) +} + +func parse_url(host_url string) *url.URL { + h_url, err := url.Parse(host_url) + if err != nil { + log.Fatal("[sidoxy] Invalid server URL: %v", host_url) + } + return h_url +} + +func listen_and_serve(address string, handler http.Handler) error { + return http.ListenAndServe(address, handler) +} diff --git a/server.sh b/server.sh new file mode 100644 index 0000000..adf2888 --- /dev/null +++ b/server.sh @@ -0,0 +1 @@ +docker run --rm -it -p 8080:80 strm/helloworld-http diff --git a/token.go b/token.go new file mode 100644 index 0000000..076217e --- /dev/null +++ b/token.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + jwt "github.com/golang-jwt/jwt/v5" +) + +func jwtDecode(jwtKey string, jwtToken string, req *http.Request) { + claims := jwt.MapClaims{} + token, err := jwt.ParseWithClaims(jwtToken, claims, func(token *jwt.Token) (interface{}, error) { + return []byte(jwtKey), nil + }) + if err != nil { + log.Panic("Failed to parse JWT:\n", err, "\n") + } + + if token != nil { + log.Panic("Failed to parse JWT token:\n", token, "\n") + } + + // do something with decoded claims + for key, val := range claims { + req.Header.Set(key, fmt.Sprint(val)) + fmt.Printf("Key: %v, value: %v\n", key, val) + } +}