commit a86ef6fe1d5dd87962704513cc76fdd3e23e4309 Author: ghostersk Date: Sat Mar 1 11:39:07 2025 +0000 First commit 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") +}