From a86ef6fe1d5dd87962704513cc76fdd3e23e4309 Mon Sep 17 00:00:00 2001 From: ghostersk Date: Sat, 1 Mar 2025 11:39:07 +0000 Subject: [PATCH] First commit --- README.md | 11 ++++ certificate.go | 125 +++++++++++++++++++++++++++++++++++++++ config.go | 154 +++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 5 ++ go.sum | 4 ++ main.go | 105 +++++++++++++++++++++++++++++++++ proxy.go | 93 +++++++++++++++++++++++++++++ utils.go | 10 ++++ 8 files changed, 507 insertions(+) create mode 100644 README.md create mode 100644 certificate.go create mode 100644 config.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 proxy.go create mode 100644 utils.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..77c48d9 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# GoLangProxy +- simple application written in go lang for proxing http and https with built in self signed certificate function. + +### setup project +go mod init proxy + +### Running Proxy app without compiling. +go run main.go config.go certificate.go proxy.go utils.go + +### Building app: +go build -o proxy diff --git a/certificate.go b/certificate.go new file mode 100644 index 0000000..865e7aa --- /dev/null +++ b/certificate.go @@ -0,0 +1,125 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "log" + "math/big" + "os" + "time" +) + +func generateSelfSignedCert() error { + if err := os.MkdirAll(certDir, 0755); err != nil { + return fmt.Errorf("failed to create certificate directory %s: %v", certDir, err) + } + + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return fmt.Errorf("failed to generate private key: %v", err) + } + + notBefore := time.Now() + notAfter := notBefore.Add(365 * 24 * time.Hour) + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return fmt.Errorf("failed to generate serial number: %v", err) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Proxy Self-Signed"}, + CommonName: "localhost", + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: []string{"localhost", "*.example.com"}, + } + + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return fmt.Errorf("failed to create certificate: %v", err) + } + + certOut, err := os.Create(certPath) + if err != nil { + return fmt.Errorf("failed to open %s for writing: %v", certPath, err) + } + if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}); err != nil { + certOut.Close() + return fmt.Errorf("failed to encode certificate: %v", err) + } + certOut.Close() + + keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf("failed to open %s for writing: %v", keyPath, err) + } + if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil { + keyOut.Close() + return fmt.Errorf("failed to encode private key: %v", err) + } + keyOut.Close() + + log.Printf("Generated self-signed certificate in %s", certDir) + return nil +} + +func loadCertificate() error { + certFile, err := os.ReadFile(certPath) + if err != nil { + return fmt.Errorf("failed to read certificate %s: %v", certPath, err) + } + keyFile, err := os.ReadFile(keyPath) + if err != nil { + return fmt.Errorf("failed to read key %s: %v", keyPath, err) + } + + newCert, err := tls.X509KeyPair(certFile, keyFile) + if err != nil { + return fmt.Errorf("failed to parse certificate: %v", err) + } + + configMux.Lock() + cert = &newCert + configMux.Unlock() + return nil +} + +func monitorCertificates() { + var lastModTime time.Time + for { + certInfo, err := os.Stat(certPath) + if err != nil { + log.Printf("Error checking certificate: %v", err) + time.Sleep(5 * time.Second) + continue + } + + keyInfo, err := os.Stat(keyPath) + if err != nil { + log.Printf("Error checking key: %v", err) + time.Sleep(5 * time.Second) + continue + } + + if certInfo.ModTime() != lastModTime || keyInfo.ModTime() != lastModTime { + if err := loadCertificate(); err != nil { + log.Printf("Error reloading certificate: %v", err) + } else { + log.Println("Certificate reloaded successfully") + lastModTime = certInfo.ModTime() + } + } + time.Sleep(5 * time.Second) + } +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..7181c1f --- /dev/null +++ b/config.go @@ -0,0 +1,154 @@ +package main + +import ( + "fmt" + "log" + "os" + "time" + + "gopkg.in/yaml.v2" +) + +type Config struct { + ListenHTTP string `yaml:"listen_http"` + ListenHTTPS string `yaml:"listen_https"` + CertDir string `yaml:"cert_dir"` + CertFile string `yaml:"cert_file"` + KeyFile string `yaml:"key_file"` + Routes map[string]string `yaml:"routes"` + TrustTarget map[string]bool `yaml:"trust_target"` + NoHTTPSRedirect map[string]bool `yaml:"no_https_redirect"` // New field +} + +func loadConfig() (Config, error) { + data, err := os.ReadFile(configPath) + if err != nil { + return Config{}, fmt.Errorf("failed to read config %s: %v", configPath, err) + } + + var cfg Config + err = yaml.Unmarshal(data, &cfg) + if err != nil { + return Config{}, fmt.Errorf("failed to unmarshal config: %v", err) + } + return cfg, nil +} + +func generateDefaultConfig() Config { + return Config{ + ListenHTTP: ":80", + ListenHTTPS: ":443", + CertDir: "./certificates", // default current directory creates certificates folder + CertFile: "certificate.pem", + KeyFile: "key.pem", + Routes: map[string]string{ + "*": "http://127.0.0.1:80", + "main.example.com": "http://127.0.0.1:80", + }, + TrustTarget: map[string]bool{ + "*": true, // default trust all certificates + "main.example.com": false, // use only trusted certificate + }, + NoHTTPSRedirect: map[string]bool{ + "*": false, // Default: redirect to HTTPS + "main.example.com": true, // set to not redirect HTTP to HTTPS + }, + } +} + +func saveConfig(cfg Config) error { + if err := os.MkdirAll(baseDir, 0755); err != nil { + return fmt.Errorf("failed to create base directory %s: %v", baseDir, err) + } + + data, err := yaml.Marshal(cfg) + if err != nil { + return fmt.Errorf("failed to marshal config: %v", err) + } + + err = os.WriteFile(configPath, data, 0644) + if err != nil { + return fmt.Errorf("failed to write config to %s: %v", configPath, err) + } + return nil +} + +func monitorConfig() { + var lastModTime time.Time + for { + configInfo, err := os.Stat(configPath) + if err != nil { + log.Printf("Error checking config file: %v", err) + time.Sleep(5 * time.Second) + continue + } + + if configInfo.ModTime() != lastModTime { + newConfig, err := loadConfig() + if err != nil { + log.Printf("Error reloading config: %v", err) + } else { + configMux.Lock() + if newConfig.ListenHTTP != config.ListenHTTP { + config.ListenHTTP = newConfig.ListenHTTP + log.Printf("Updated listen_http to %s", config.ListenHTTP) + } + if newConfig.ListenHTTPS != config.ListenHTTPS { + config.ListenHTTPS = newConfig.ListenHTTPS + log.Printf("Updated listen_https to %s", config.ListenHTTPS) + } + if newConfig.CertDir != config.CertDir || newConfig.CertFile != config.CertFile || newConfig.KeyFile != config.KeyFile { + config.CertDir = newConfig.CertDir + config.CertFile = newConfig.CertFile + config.KeyFile = newConfig.KeyFile + updatePaths() + if err := loadCertificate(); err != nil { + log.Printf("Error reloading certificate after path change: %v", err) + } else { + log.Println("Updated certificate paths and reloaded certificate") + } + } + for k, v := range newConfig.Routes { + if oldV, exists := config.Routes[k]; !exists || oldV != v { + config.Routes[k] = v + log.Printf("Updated route %s to %s", k, v) + } + } + for k := range config.Routes { + if _, exists := newConfig.Routes[k]; !exists { + delete(config.Routes, k) + log.Printf("Removed route %s", k) + } + } + for k, v := range newConfig.TrustTarget { + if oldV, exists := config.TrustTarget[k]; !exists || oldV != v { + config.TrustTarget[k] = v + log.Printf("Updated trust_target %s to %v", k, v) + } + } + for k := range config.TrustTarget { + if _, exists := newConfig.TrustTarget[k]; !exists { + delete(config.TrustTarget, k) + log.Printf("Removed trust_target %s", k) + } + } + for k, v := range newConfig.NoHTTPSRedirect { + if oldV, exists := config.NoHTTPSRedirect[k]; !exists || oldV != v { + config.NoHTTPSRedirect[k] = v + log.Printf("Updated no_https_redirect %s to %v", k, v) + } + } + for k := range config.NoHTTPSRedirect { + if _, exists := newConfig.NoHTTPSRedirect[k]; !exists { + delete(config.NoHTTPSRedirect, k) + log.Printf("Removed no_https_redirect %s", k) + } + } + configMux.Unlock() + log.Println("Config reloaded successfully") + lastModTime = configInfo.ModTime() + } + } + time.Sleep(5 * time.Second) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4a28884 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module main + +go 1.24.0 + +require gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..dd0bc19 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..1bf02a4 --- /dev/null +++ b/main.go @@ -0,0 +1,105 @@ +package main + +// go run main.go config.go certificate.go proxy.go utils.go +// go build -o proxy +import ( + "crypto/tls" + "log" + "net/http" + "os" + "path/filepath" + "runtime" + "sync" +) + +// Shared global variables (declared only here) +var ( + config Config + configMux sync.RWMutex + cert *tls.Certificate + baseDir string + certDir string + certPath string + keyPath string + configPath string +) + +func init() { + if runtime.GOOS == "windows" && len(os.Args) > 0 && filepath.Ext(os.Args[0]) == ".go" { + var err error + baseDir, err = os.Getwd() + if err != nil { + log.Fatalf("Failed to get working directory: %v", err) + } + } else { + exePath, err := os.Executable() + if err != nil { + log.Fatalf("Failed to get executable path: %v", err) + } + baseDir = filepath.Dir(exePath) + } +} + +func main() { + configPath = filepath.Join(baseDir, "config.yaml") + if _, err := os.Stat(configPath); os.IsNotExist(err) { + config = generateDefaultConfig() + if err := saveConfig(config); err != nil { + log.Fatalf("Failed to save default config: %v", err) + } + log.Println("Generated default config file") + } else { + cfg, err := loadConfig() + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } + config = cfg + } + + updatePaths() + + _, certErr := os.Stat(certPath) + _, keyErr := os.Stat(keyPath) + if os.IsNotExist(certErr) || os.IsNotExist(keyErr) { + if err := generateSelfSignedCert(); err != nil { + log.Fatalf("Failed to generate self-signed certificate: %v", err) + } + } + + if err := loadCertificate(); err != nil { + log.Fatalf("Failed to load certificate: %v", err) + } + + go monitorCertificates() + go monitorConfig() + + httpServer := &http.Server{ + Addr: config.ListenHTTP, + Handler: http.HandlerFunc(handler), + } + + tlsConfig := &tls.Config{ + GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + configMux.RLock() + defer configMux.RUnlock() + return cert, nil + }, + } + httpsServer := &http.Server{ + Addr: config.ListenHTTPS, + Handler: http.HandlerFunc(handler), + TLSConfig: tlsConfig, + } + + go func() { + log.Printf("Starting HTTP server on %s", config.ListenHTTP) + if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("HTTP server error: %v", err) + } + }() + + log.Printf("Starting HTTPS server on %s", config.ListenHTTPS) + if err := httpsServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { + log.Fatalf("HTTPS server error: %v", err) + } +} diff --git a/proxy.go b/proxy.go new file mode 100644 index 0000000..fa4f484 --- /dev/null +++ b/proxy.go @@ -0,0 +1,93 @@ +package main + +import ( + "crypto/tls" + "log" + "net/http" + "net/http/httputil" + "net/url" + "strings" +) + +func getReverseProxy(target string, skipVerify bool) *httputil.ReverseProxy { + targetURL, err := url.Parse(target) + if err != nil { + log.Printf("Error parsing target URL %s: %v", target, err) + return nil + } + + director := func(req *http.Request) { + req.URL.Scheme = targetURL.Scheme + req.URL.Host = targetURL.Host + req.Host = targetURL.Host // Set Host header to match target + + // Preserve original headers + for k, v := range req.Header { + if k != "Host" { // Host is set above + req.Header[k] = v + } + } + + // Ensure the full path is preserved + if targetURL.Path != "" { + req.URL.Path = strings.TrimPrefix(req.URL.Path, "/") // Avoid double slashes + req.URL.Path = singleJoin(targetURL.Path, req.URL.Path) + } + } + + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: skipVerify}, + } + + return &httputil.ReverseProxy{ + Director: director, + Transport: transport, + ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { + log.Printf("Proxy error for %s: %v", r.Host, err) + http.Error(w, "Proxy error", http.StatusBadGateway) + }, + } +} + +// singleJoin ensures a single slash between path segments +func singleJoin(prefix, suffix string) string { + prefix = strings.TrimSuffix(prefix, "/") + suffix = strings.TrimPrefix(suffix, "/") + return prefix + "/" + suffix +} + +func handler(w http.ResponseWriter, r *http.Request) { + configMux.RLock() + defer configMux.RUnlock() + + target, exists := config.Routes[r.Host] + skipVerify := config.TrustTarget[r.Host] + noHTTPSRedirect := config.NoHTTPSRedirect[r.Host] + + if !exists { + if target, exists = config.Routes["*"]; !exists { + http.Error(w, "Host not configured", http.StatusNotFound) + return + } + skipVerify = config.TrustTarget["*"] + noHTTPSRedirect = config.NoHTTPSRedirect["*"] + } + + // Check if request is HTTP and target is HTTPS + isHTTPS := target[:5] == "https" + isHTTPReq := r.TLS == nil // r.TLS is nil for HTTP, non-nil for HTTPS + + if isHTTPReq && isHTTPS && !noHTTPSRedirect { + // Redirect to HTTPS version of the host + redirectURL := "https://" + r.Host + r.RequestURI + http.Redirect(w, r, redirectURL, http.StatusMovedPermanently) + return + } + + proxy := getReverseProxy(target, skipVerify) + if proxy == nil { + http.Error(w, "Invalid target configuration", http.StatusInternalServerError) + return + } + proxy.ServeHTTP(w, r) +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..50c4e43 --- /dev/null +++ b/utils.go @@ -0,0 +1,10 @@ +package main + +import "path/filepath" + +func updatePaths() { + certDir = filepath.Join(baseDir, config.CertDir) + certPath = filepath.Join(certDir, config.CertFile) + keyPath = filepath.Join(certDir, config.KeyFile) + configPath = filepath.Join(baseDir, "config.yaml") +}