commit 98876106ed01889c01b225c100c1f29c5da4a360 Author: Elia el Lazkani Date: Tue Jul 11 03:10:24 2023 +0200 In the beginning the was darkness and then there was a reverse proxy. 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) + } +}