From 944b787432115078eb77e9144246599554bbb081 Mon Sep 17 00:00:00 2001 From: Yann Stepienik Date: Sun, 26 Feb 2023 22:26:09 +0000 Subject: [PATCH] Wrote proxy --- .gitignore | 3 +- .gupm_rc.gs | 3 +- gupm.json | 2 + src/config.go | 82 ++++++++++++++++++++++++++++ src/httpServer.go | 93 ++++++++++++++++++++++++++++++++ src/index.go | 100 ++--------------------------------- src/proxy/buildFromConfig.go | 33 ++++++++++++ src/proxy/routeTo.go | 51 ++++++++++++++++++ src/proxy/routerGen.go | 41 ++++++++++++++ test-server.js | 38 +++++++++++++ test-websocket.js | 16 ++++++ 11 files changed, 364 insertions(+), 98 deletions(-) create mode 100644 src/config.go create mode 100644 src/httpServer.go create mode 100644 src/proxy/buildFromConfig.go create mode 100644 src/proxy/routeTo.go create mode 100644 src/proxy/routerGen.go create mode 100644 test-server.js create mode 100644 test-websocket.js diff --git a/.gitignore b/.gitignore index b355034..a56b938 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ localcert.key .vite dev.json .bin -client/dist \ No newline at end of file +client/dist +config_dev.json \ No newline at end of file diff --git a/.gupm_rc.gs b/.gupm_rc.gs index a73a066..29efc1e 100644 --- a/.gupm_rc.gs +++ b/.gupm_rc.gs @@ -4,4 +4,5 @@ env("GOPATH", run("go", ["env", "GOROOT"]) + ":" + pwd() + "/go_modules" + ":" + // dev mode env("MONGODB", readJsonFile("dev.json").MONGODB) env("HTTP_PORT", 8080) -env("HTTPS_PORT", 8443) \ No newline at end of file +env("HTTPS_PORT", 8443) +env("CONFIG_FILE", "./config_dev.json") \ No newline at end of file diff --git a/gupm.json b/gupm.json index 9273575..d44c880 100644 --- a/gupm.json +++ b/gupm.json @@ -20,6 +20,8 @@ "go://gopkg.in/ffmt.v1": "v1.5.6", "npm://@esbuild/linux-x64": "0.16.17", "npm://@vitejs/plugin-react": "3.1.0", + "npm://express": "4.18.2", + "npm://express-ws": "5.0.2", "npm://react": "18.2.0", "npm://react-dom": "18.2.0", "npm://typescript": "4.9.5", diff --git a/src/config.go b/src/config.go new file mode 100644 index 0000000..7d8de8f --- /dev/null +++ b/src/config.go @@ -0,0 +1,82 @@ +package main + +import ( + "log" + "os" + "regexp" + "./proxy" + "encoding/json" +) + +type Config struct { + HTTPConfig HTTPConfig +} + +var defaultConfig = Config{ + HTTPConfig: HTTPConfig{ + TLSCert: "localcert.crt", + TLSKey: "localcert.key", + GenerateMissingTLSCert: true, + HTTPPort: "80", + HTTPSPort: "443", + ProxyConfig: proxy.Config{ + Routes: []proxy.RouteConfig{}, + }, + }, +} + +func GetConfig() Config { + configFile := os.Getenv("CONFIG_FILE") + + if configFile == "" { + configFile = "/cosmos.config.json" + } + + log.Println("Using config file: " + configFile) + + // if file does not exist, create it + if _, err := os.Stat(configFile); os.IsNotExist(err) { + log.Println("Config file does not exist. Creating default config file.") + file, err := os.Create(configFile) + if err != nil { + log.Fatal("[ERROR] Creating Default Config File: " + err.Error()) + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + err = encoder.Encode(defaultConfig) + if err != nil { + log.Fatal("[ERROR] Writing Default Config File: " + err.Error()) + } + + return defaultConfig + } + + file, err := os.Open(configFile) + if err != nil { + log.Fatal("[ERROR] Opening Config File: " + err.Error()) + } + defer file.Close() + + decoder := json.NewDecoder(file) + config := Config{} + err = decoder.Decode(&config) + // check file is not empty + if err != nil { + // check error is not empty + if err.Error() == "EOF" { + log.Fatal("[ERROR] Reading Config File: File is empty.") + } + + // get error string + errString := err.Error() + + // replace string in error + m1 := regexp.MustCompile(`json: cannot unmarshal ([A-Za-z\.]+) into Go struct field ([A-Za-z\.]+) of type ([A-Za-z\.]+)`) + errString = m1.ReplaceAllString(errString, "Invalid JSON in config file.\n > Field $2 is wrong.\n > Type is $1 Should be $3") + log.Fatal("[ERROR] Reading Config File: " + errString) + } + + return config +} \ No newline at end of file diff --git a/src/httpServer.go b/src/httpServer.go new file mode 100644 index 0000000..8d4d70b --- /dev/null +++ b/src/httpServer.go @@ -0,0 +1,93 @@ +package main + +import ( + "net/http" + "./utils" + // "./file" + // "./user" + "./proxy" + "github.com/gorilla/mux" + "log" + "os" + "strings" +) + +type HTTPConfig struct { + TLSCert string + TLSKey string + GenerateMissingTLSCert bool + HTTPPort string + HTTPSPort string + ProxyConfig proxy.Config +} + +var serverPortHTTP = os.Getenv("HTTP_PORT") +var serverPortHTTPS = os.Getenv("HTTPS_PORT") + +func startHTTPServer(router *mux.Router) { + log.Println("Listening to HTTP on :" + serverPortHTTP) + + err := http.ListenAndServe("0.0.0.0:" + serverPortHTTP, router) + + if err != nil { + log.Fatal(err) + } +} + +func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) { + // redirect http to https + go (func () { + err := http.ListenAndServe("0.0.0.0:" + serverPortHTTP, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // change port in host + if strings.HasSuffix(r.Host, ":" + serverPortHTTP) { + if serverPortHTTPS != "443" { + r.Host = r.Host[:len(r.Host)-len(":" + serverPortHTTP)] + ":" + serverPortHTTPS + } else { + r.Host = r.Host[:len(r.Host)-len(":" + serverPortHTTP)] + } + } + + http.Redirect(w, r, "https://"+r.Host+r.URL.String(), http.StatusMovedPermanently) + })) + if err != nil { + log.Fatal(err) + } + })() + + log.Println("Listening to HTTP on :" + serverPortHTTP) + log.Println("Listening to HTTPS on :" + serverPortHTTPS) + + // start https server + err := http.ListenAndServeTLS("0.0.0.0:" + serverPortHTTPS, tlsCert, tlsKey, router) + + if err != nil { + log.Fatal(err) + } +} + + +func StartServer(config HTTPConfig) { + var tlsCert = config.TLSCert + var tlsKey= config.TLSKey + + if serverPortHTTP == "" { + serverPortHTTP = config.HTTPPort + } + + if serverPortHTTPS == "" { + serverPortHTTPS = config.HTTPSPort + } + + router := proxy.BuildFromConfig(config.ProxyConfig) + + if utils.FileExists(tlsCert) && utils.FileExists(tlsKey) { + log.Println("TLS certificate found, starting HTTPS servers and redirecting HTTP to HTTPS") + startHTTPSServer(router, tlsCert, tlsKey) + } else { + log.Println("No TLS certificate found, starting HTTP server only") + startHTTPServer(router) + } +} + +func StopServer() { +} \ No newline at end of file diff --git a/src/index.go b/src/index.go index c2aec85..d1c39a3 100644 --- a/src/index.go +++ b/src/index.go @@ -1,106 +1,14 @@ package main import ( - "net/http" - "./utils" - "./file" - "./user" - "github.com/gorilla/mux" - "log" - "os" - "strings" + "log" ) -var tlsCert = "localcert.crt" -var tlsKey= "localcert.key" -var serverPortHTTP = os.Getenv("HTTP_PORT") -var serverPortHTTPS = os.Getenv("HTTPS_PORT") - -func startHTTPServer(router *mux.Router) { - log.Println("Listening to HTTP on :" + serverPortHTTP) - - err := http.ListenAndServe("0.0.0.0:" + serverPortHTTP, router) - - if err != nil { - log.Fatal(err) - } -} - -func startHTTPSServer(router *mux.Router) { - // redirect http to https - go (func () { - err := http.ListenAndServe("0.0.0.0:" + serverPortHTTP, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // change port in host - if strings.HasSuffix(r.Host, ":" + serverPortHTTP) { - if serverPortHTTPS != "443" { - r.Host = r.Host[:len(r.Host)-len(":" + serverPortHTTP)] + ":" + serverPortHTTPS - } else { - r.Host = r.Host[:len(r.Host)-len(":" + serverPortHTTP)] - } - } - - http.Redirect(w, r, "https://"+r.Host+r.URL.String(), http.StatusMovedPermanently) - })) - if err != nil { - log.Fatal(err) - } - })() - - log.Println("Listening to HTTP on :" + serverPortHTTP) - log.Println("Listening to HTTPS on :" + serverPortHTTPS) - - // start https server - err := http.ListenAndServeTLS("0.0.0.0:" + serverPortHTTPS, tlsCert, tlsKey, router) - - if err != nil { - log.Fatal(err) - } -} - func main() { log.Println("Starting...") - if serverPortHTTP == "" { - serverPortHTTP = "80" - } + config := GetConfig() - if serverPortHTTPS == "" { - serverPortHTTPS = "443" - } - - router := mux.NewRouter().StrictSlash(true) - - utils.DB() - defer utils.Disconnect() - - router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("OK")) - }) - - router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) - - router.HandleFunc("/file/list", file.FileList) - router.HandleFunc("/file/get", file.FileGet) - router.HandleFunc("/file/delete", file.FileDelete) - router.HandleFunc("/file/copy", file.FileCopy) - router.HandleFunc("/file/move", file.FileMove) - - router.HandleFunc("/user/login", user.UserLogin) - // router.HandleFunc("/user/register", user.UserRegister) - // router.HandleFunc("/user/edit", ) - // router.HandleFunc("/user/delete", ) - - // router.HandleFunc("/config/get", ) - // router.HandleFunc("/config/set", ) - - // router.HandleFunc("/db", ) - - if utils.FileExists(tlsCert) && utils.FileExists(tlsKey) { - log.Println("TLS certificate found, starting HTTPS servers and redirecting HTTP to HTTPS") - startHTTPSServer(router) - } else { - log.Println("No TLS certificate found, starting HTTP server only") - startHTTPServer(router) - } + defer StopServer() + StartServer(config.HTTPConfig) } \ No newline at end of file diff --git a/src/proxy/buildFromConfig.go b/src/proxy/buildFromConfig.go new file mode 100644 index 0000000..86af3ff --- /dev/null +++ b/src/proxy/buildFromConfig.go @@ -0,0 +1,33 @@ +package proxy + +import ( + "github.com/gorilla/mux" + "net/http" +) + +type RouteConfig struct { + Routing Route + Target string +} + +type Config struct { + Routes []RouteConfig +} + +func BuildFromConfig(config Config) *mux.Router { + router := mux.NewRouter().StrictSlash(true) + + router.HandleFunc("/_health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + }) + + router.PathPrefix("/_static").Handler(http.StripPrefix("/_static", http.FileServer(http.Dir("static")))) + + for i := len(config.Routes)-1; i >= 0; i-- { + routeConfig := config.Routes[i] + RouterGen(routeConfig.Routing, router, RouteTo(routeConfig.Target)) + } + + return router +} \ No newline at end of file diff --git a/src/proxy/routeTo.go b/src/proxy/routeTo.go new file mode 100644 index 0000000..6dec274 --- /dev/null +++ b/src/proxy/routeTo.go @@ -0,0 +1,51 @@ +package proxy + +import ( + "net/http" + "net/http/httputil" + "net/url" + "log" + // "io/ioutil" + // "io" + // "os" + // "golang.org/x/crypto/bcrypt" + + // "../utils" +) + +// NewProxy takes target host and creates a reverse proxy +func NewProxy(targetHost string) (*httputil.ReverseProxy, error) { + url, err := url.Parse(targetHost) + if err != nil { + return nil, err + } + + proxy := httputil.NewSingleHostReverseProxy(url) + + // upgrade the request to websocket + proxy.ModifyResponse = func(resp *http.Response) error { + log.Println("[INFO] Response from backend: ", resp.Status) + log.Println("[INFO] URL was ", resp.Request.URL) + return nil + } + + return proxy, nil +} + +// ProxyRequestHandler handles the http request using proxy +func ProxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + proxy.ServeHTTP(w, r) + } +} + +func RouteTo(destination string) *httputil.ReverseProxy /*func(http.ResponseWriter, *http.Request)*/ { + // initialize a reverse proxy and pass the actual backend server url here + proxy, err := NewProxy(destination) + if err != nil { + panic(err) + } + + // create a handler function which uses the reverse proxy + return proxy //ProxyRequestHandler(proxy) +} \ No newline at end of file diff --git a/src/proxy/routerGen.go b/src/proxy/routerGen.go new file mode 100644 index 0000000..936654c --- /dev/null +++ b/src/proxy/routerGen.go @@ -0,0 +1,41 @@ +package proxy + +import ( + "net/http" + "net/http/httputil" + "github.com/gorilla/mux" + // "log" + // "io/ioutil" + // "io" + // "os" + // "golang.org/x/crypto/bcrypt" + + // "../utils" +) + +type Route struct { + UseHost bool + Host string + UsePathPrefix bool + PathPrefix string +} + +func RouterGen(route Route, router *mux.Router, destination *httputil.ReverseProxy) *mux.Route { + var realDestination http.Handler + realDestination = destination + + origin := router.Methods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD") + + if(route.UseHost) { + origin = origin.Host(route.Host) + } + + if(route.UsePathPrefix) { + origin = origin.PathPrefix(route.PathPrefix) + realDestination = http.StripPrefix(route.PathPrefix, destination) + } + + origin.Handler(realDestination) + + return origin +} \ No newline at end of file diff --git a/test-server.js b/test-server.js new file mode 100644 index 0000000..f19835d --- /dev/null +++ b/test-server.js @@ -0,0 +1,38 @@ + +const express = require('express') +const app = express() +const port = 3000 +var expressWs = require('express-ws')(app); + +// console log every request sent +app.use((req, res, next) => { + console.log(`[REQ] - ${req.method} ${req.url}`) + next() +}); + +app.get('/return/:status/:time', async (req, res) => { + const statusCode = parseInt(req.params.status); + const returnString =`Hello status ${statusCode} after ${req.params.time}ms !` + + console.log(`[RES] - ${statusCode} ${returnString}`) + + await new Promise(resolve => setTimeout(resolve, req.params.time)); + + return res.status(statusCode).send(returnString) +}); + +app.get('/', (req, res) => { + console.log("[RES] - Hello World!") + res.send('Hello World!') +}) + +app.listen(port, () => { + console.log(`Example app listening on port ${port}`) +}) + +app.ws('/ws', function(ws, req) { + ws.on('message', function(msg) { + console.log(msg); + ws.send(msg); + }); +}); \ No newline at end of file diff --git a/test-websocket.js b/test-websocket.js new file mode 100644 index 0000000..3b59b8b --- /dev/null +++ b/test-websocket.js @@ -0,0 +1,16 @@ +const WebSocket = require('ws'); + +// send + +const ws = new WebSocket('ws://localhost:8080/proxy/ws', { + rejectUnauthorized: false, + followRedirects : true, + }); + +ws.on('open', function open() { + ws.send('something'); +}); + +ws.on('message', function incoming(data) { + console.log(data); +});