updated app build
This commit is contained in:
BIN
golangproxy/build/icon.ico
Normal file
BIN
golangproxy/build/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
BIN
golangproxy/build/icon.png
Normal file
BIN
golangproxy/build/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
16
golangproxy/config.yaml
Normal file
16
golangproxy/config.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
listen_http: :80
|
||||
listen_https: :443
|
||||
cert_file: ./crt/certificate.pem
|
||||
key_file: ./crt/key.pem
|
||||
routes:
|
||||
'*': http://127.0.0.1:61147
|
||||
gg.example.com: https://example.com:443
|
||||
main.example.com: https://10.100.111.254:4444
|
||||
trust_target:
|
||||
'*': true
|
||||
gg.example.com: false
|
||||
main.example.com: true
|
||||
no_https_redirect:
|
||||
'*': false
|
||||
gg.example.com: true
|
||||
main.example.com: false
|
||||
65
golangproxy/config/config.go
Normal file
65
golangproxy/config/config.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Config represents the application configuration
|
||||
type Config struct {
|
||||
ListenHTTP string `yaml:"listen_http"` // HTTP listen address (e.g., ":80")
|
||||
ListenHTTPS string `yaml:"listen_https"` // HTTPS listen address (e.g., ":443")
|
||||
CertFile string `yaml:"cert_file"` // Path to SSL certificate
|
||||
KeyFile string `yaml:"key_file"` // Path to SSL key
|
||||
Routes map[string]string `yaml:"routes"` // Host to target URL mappings
|
||||
TrustTarget map[string]bool `yaml:"trust_target"` // Whether to trust invalid target certs
|
||||
NoHTTPSRedirect map[string]bool `yaml:"no_https_redirect"` // Disable HTTP to HTTPS redirect
|
||||
}
|
||||
|
||||
// LoadConfig loads the config from file or creates a default one
|
||||
func LoadConfig(configPath string) (*Config, error) {
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
// Create default configuration
|
||||
defaultConfig := &Config{
|
||||
ListenHTTP: ":80",
|
||||
ListenHTTPS: ":443",
|
||||
CertFile: "./crt/certificate.pem",
|
||||
KeyFile: "./crt/key.pem",
|
||||
Routes: map[string]string{
|
||||
"*": "http://127.0.0.1:61147", // accespt all route
|
||||
"main.example.com": "https://10.100.111.254:4444", // Specific route
|
||||
"gg.example.com": "https://example.com:443",
|
||||
},
|
||||
TrustTarget: map[string]bool{
|
||||
"*": true, // true = trust any certificates on target url
|
||||
"main.example.com": true,
|
||||
"gg.example.com": false, // trusting target cetificate disabled
|
||||
},
|
||||
NoHTTPSRedirect: map[string]bool{
|
||||
"*": false, // false = HTTP redirected to HTTPS automatically
|
||||
"main.example.com": false,
|
||||
"gg.example.com": true, // no automatic redirect to HTTPS from HTTP
|
||||
},
|
||||
}
|
||||
data, err := yaml.Marshal(defaultConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return defaultConfig, nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
24
golangproxy/go.mod
Normal file
24
golangproxy/go.mod
Normal file
@@ -0,0 +1,24 @@
|
||||
module golangproxy
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.8.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/akavel/rsrc v0.10.2 // indirect
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
|
||||
github.com/getlantern/systray v1.2.2 // indirect
|
||||
github.com/go-stack/stack v1.8.0 // indirect
|
||||
github.com/josephspurrier/goversioninfo v1.4.1 // indirect
|
||||
github.com/kardianos/service v1.2.2 // indirect
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
)
|
||||
45
golangproxy/go.sum
Normal file
45
golangproxy/go.sum
Normal file
@@ -0,0 +1,45 @@
|
||||
github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=
|
||||
github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
||||
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/josephspurrier/goversioninfo v1.4.1 h1:5LvrkP+n0tg91J9yTkoVnt/QgNnrI1t4uSsWjIonrqY=
|
||||
github.com/josephspurrier/goversioninfo v1.4.1/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY=
|
||||
github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60=
|
||||
github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||
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=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
23
golangproxy/info.md
Normal file
23
golangproxy/info.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# structure:
|
||||
```
|
||||
/golangproxy
|
||||
├── main.go # Application entry point
|
||||
├── config/
|
||||
│ └── config.go # Configuration loading and parsing
|
||||
├── proxy/
|
||||
│ └── proxy.go # Reverse proxy logic
|
||||
├── server/
|
||||
│ └── server.go # Simple web server implementation
|
||||
├── ssl/
|
||||
│ └── ssl.go # SSL certificate management
|
||||
├── logger/
|
||||
│ └── logger.go # Logging setup
|
||||
├── logs/ # Logs directory (created at runtime)
|
||||
├── ssl/ # SSL certificates directory (created at runtime)
|
||||
├── www/ # Web server content directory (created at runtime)
|
||||
└── tests/ # Test files
|
||||
├── config_test.go # Tests for config package
|
||||
├── proxy_test.go # Tests for proxy package
|
||||
├── server_test.go # Tests for server package
|
||||
└── ssl_test.go # Tests for ssl package
|
||||
```
|
||||
40
golangproxy/logger/logger.go
Normal file
40
golangproxy/logger/logger.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Logger is the global logger instance
|
||||
var Logger *log.Logger
|
||||
|
||||
// InitLogger initializes logging to file and stdout
|
||||
func InitLogger() {
|
||||
if err := os.MkdirAll("logs", 0755); err != nil {
|
||||
log.Fatalf("Error creating logs directory: %v", err)
|
||||
}
|
||||
logFile, err := os.OpenFile(filepath.Join("logs", "proxy.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("Error opening log file: %v", err)
|
||||
}
|
||||
multiWriter := io.MultiWriter(os.Stdout, logFile)
|
||||
Logger = log.New(multiWriter, "", log.LstdFlags)
|
||||
// Wrap the logger to filter context canceled errors
|
||||
oldOutput := Logger.Writer()
|
||||
Logger.SetOutput(&filteredWriter{Writer: oldOutput})
|
||||
}
|
||||
|
||||
// filteredWriter wraps an io.Writer to filter out context canceled errors
|
||||
type filteredWriter struct {
|
||||
Writer io.Writer
|
||||
}
|
||||
|
||||
func (fw *filteredWriter) Write(p []byte) (n int, err error) {
|
||||
if strings.Contains(string(p), "context canceled") && strings.Contains(string(p), "http: proxy error") {
|
||||
return len(p), nil // Silently discard the message
|
||||
}
|
||||
return fw.Writer.Write(p)
|
||||
}
|
||||
345
golangproxy/main.go
Normal file
345
golangproxy/main.go
Normal file
@@ -0,0 +1,345 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
|
||||
"golangproxy/config"
|
||||
"golangproxy/logger"
|
||||
"golangproxy/proxy"
|
||||
"golangproxy/server"
|
||||
"golangproxy/ssl"
|
||||
)
|
||||
|
||||
// Global variables for dynamic configuration and certificate updates
|
||||
var (
|
||||
configPath = "config.yaml"
|
||||
routesMutex sync.RWMutex // Protects routes and defaultRoute
|
||||
certMutex sync.RWMutex // Protects currentCert
|
||||
currentConfig *config.Config // Current configuration
|
||||
currentCert *tls.Certificate // Current SSL certificate
|
||||
routes map[string]*proxy.Route // Host-specific routes
|
||||
defaultRoute *proxy.Route // Wildcard route
|
||||
watcher *fsnotify.Watcher // File watcher instance
|
||||
)
|
||||
|
||||
// main initializes and runs the reverse proxy application
|
||||
func main() {
|
||||
// Initialize logging to file and terminal
|
||||
logger.InitLogger()
|
||||
log := logger.Logger
|
||||
|
||||
// Load initial configuration
|
||||
var err error
|
||||
currentConfig, err = config.LoadConfig(configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading config: %v", err)
|
||||
}
|
||||
|
||||
// Ensure SSL certificate and key files exist
|
||||
err = ssl.EnsureCertFiles(currentConfig.CertFile, currentConfig.KeyFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Error ensuring cert files: %v", err)
|
||||
}
|
||||
|
||||
// Load initial SSL certificate
|
||||
cert, err := tls.LoadX509KeyPair(currentConfig.CertFile, currentConfig.KeyFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading cert: %v", err)
|
||||
}
|
||||
certMutex.Lock()
|
||||
currentCert = &cert
|
||||
certMutex.Unlock()
|
||||
|
||||
// Initialize proxy routes from config
|
||||
initializeRoutes(log)
|
||||
|
||||
// Start the simple web server in a goroutine
|
||||
go server.StartServer()
|
||||
|
||||
// Configure HTTP server
|
||||
httpServer := &http.Server{
|
||||
Addr: currentConfig.ListenHTTP,
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
routesMutex.RLock()
|
||||
route := getRoute(r.Host)
|
||||
routesMutex.RUnlock()
|
||||
if strings.HasPrefix(route.Target, "https://") && !route.NoHTTPSRedirect {
|
||||
httpsURL := "https://" + r.Host + r.URL.Path
|
||||
if r.URL.RawQuery != "" {
|
||||
httpsURL += "?" + r.URL.RawQuery
|
||||
}
|
||||
http.Redirect(w, r, httpsURL, http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
route.Handler.ServeHTTP(w, r) // Use Handler instead of Proxy
|
||||
}),
|
||||
ErrorLog: logger.Logger, // Add this to filter server-level errors (from previous fix)
|
||||
}
|
||||
|
||||
// Configure HTTPS server
|
||||
httpsServer := &http.Server{
|
||||
Addr: currentConfig.ListenHTTPS,
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
routesMutex.RLock()
|
||||
route := getRoute(r.Host)
|
||||
routesMutex.RUnlock()
|
||||
route.Handler.ServeHTTP(w, r) // Use Handler instead of Proxy
|
||||
}),
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
certMutex.RLock()
|
||||
defer certMutex.RUnlock()
|
||||
return currentCert, nil
|
||||
},
|
||||
},
|
||||
ErrorLog: logger.Logger, // Add this to filter server-level errors (from previous fix)
|
||||
}
|
||||
|
||||
// Start servers in goroutines
|
||||
go func() {
|
||||
log.Println("Starting HTTP server on", currentConfig.ListenHTTP)
|
||||
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("HTTP server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
log.Println("Starting HTTPS server on", currentConfig.ListenHTTPS)
|
||||
if err := httpsServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("HTTPS server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Initialize file watcher
|
||||
watcher, err = fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating watcher: %v", err)
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
// Watch initial config and cert files
|
||||
err = watcher.Add(configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Error watching config file: %v", err)
|
||||
}
|
||||
err = watcher.Add(currentConfig.CertFile)
|
||||
if err != nil {
|
||||
log.Println("Error watching cert file:", err)
|
||||
}
|
||||
err = watcher.Add(currentConfig.KeyFile)
|
||||
if err != nil {
|
||||
log.Println("Error watching key file:", err)
|
||||
}
|
||||
|
||||
// Handle file updates in a goroutine
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||
switch event.Name {
|
||||
case configPath:
|
||||
log.Println("Config file changed, reloading...")
|
||||
reloadConfig(log)
|
||||
case currentConfig.CertFile, currentConfig.KeyFile:
|
||||
log.Println("Cert files changed, reloading cert...")
|
||||
reloadCert(log)
|
||||
}
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Println("Watcher error:", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle graceful shutdown
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
log.Println("Shutting down...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := httpServer.Shutdown(ctx); err != nil {
|
||||
log.Println("HTTP server shutdown error:", err)
|
||||
}
|
||||
if err := httpsServer.Shutdown(ctx); err != nil {
|
||||
log.Println("HTTPS server shutdown error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// getRoute retrieves the appropriate proxy route for a host
|
||||
func getRoute(host string) *proxy.Route {
|
||||
routesMutex.RLock()
|
||||
defer routesMutex.RUnlock()
|
||||
if route, ok := routes[host]; ok {
|
||||
return route
|
||||
}
|
||||
return defaultRoute
|
||||
}
|
||||
|
||||
// initializeRoutes sets up the routes map and default route from the current config
|
||||
func initializeRoutes(log *log.Logger) {
|
||||
routesMutex.Lock()
|
||||
defer routesMutex.Unlock()
|
||||
|
||||
routes = make(map[string]*proxy.Route)
|
||||
for host, target := range currentConfig.Routes {
|
||||
if host == "*" {
|
||||
continue
|
||||
}
|
||||
trust := getConfigBool(currentConfig.TrustTarget, host)
|
||||
noRedirect := getConfigBool(currentConfig.NoHTTPSRedirect, host)
|
||||
route := proxy.CreateRoute(target, trust)
|
||||
route.NoHTTPSRedirect = noRedirect
|
||||
routes[host] = route
|
||||
}
|
||||
defaultTarget, ok := currentConfig.Routes["*"]
|
||||
if !ok {
|
||||
log.Fatal("Default route '*' not found in config")
|
||||
}
|
||||
defaultTrust := currentConfig.TrustTarget["*"]
|
||||
defaultNoRedirect := currentConfig.NoHTTPSRedirect["*"]
|
||||
defaultRoute = proxy.CreateRoute(defaultTarget, defaultTrust)
|
||||
defaultRoute.NoHTTPSRedirect = defaultNoRedirect
|
||||
}
|
||||
|
||||
// getConfigBool retrieves a boolean config value, falling back to '*' if host-specific value is absent
|
||||
func getConfigBool(m map[string]bool, host string) bool {
|
||||
if val, ok := m[host]; ok {
|
||||
return val
|
||||
}
|
||||
return m["*"]
|
||||
}
|
||||
|
||||
// reloadConfig reloads the configuration and updates routes and certs if necessary
|
||||
func reloadConfig(log *log.Logger) {
|
||||
newConfig, err := config.LoadConfig(configPath)
|
||||
if err != nil {
|
||||
log.Println("Error reloading config:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Log differences between old and new config
|
||||
log.Println("Config file changed, reloading...")
|
||||
logConfigChanges(log, currentConfig, newConfig)
|
||||
|
||||
// Store old cert file paths before updating config
|
||||
oldCertFile := currentConfig.CertFile
|
||||
oldKeyFile := currentConfig.KeyFile
|
||||
certChanged := newConfig.CertFile != oldCertFile || newConfig.KeyFile != oldKeyFile
|
||||
|
||||
currentConfig = newConfig
|
||||
|
||||
// Update routes
|
||||
initializeRoutes(log)
|
||||
|
||||
// Update certificates and watcher if paths changed
|
||||
if certChanged {
|
||||
reloadCert(log)
|
||||
updateCertWatchers(log, oldCertFile, oldKeyFile)
|
||||
}
|
||||
}
|
||||
|
||||
// logConfigChanges logs the differences between old and new config
|
||||
func logConfigChanges(log *log.Logger, oldConfig, newConfig *config.Config) {
|
||||
if oldConfig.ListenHTTP != newConfig.ListenHTTP {
|
||||
log.Printf("listen_http changed from %s to %s", oldConfig.ListenHTTP, newConfig.ListenHTTP)
|
||||
}
|
||||
if oldConfig.ListenHTTPS != newConfig.ListenHTTPS {
|
||||
log.Printf("listen_https changed from %s to %s", oldConfig.ListenHTTPS, newConfig.ListenHTTPS)
|
||||
}
|
||||
if oldConfig.CertFile != newConfig.CertFile {
|
||||
log.Printf("cert_file changed from %s to %s", oldConfig.CertFile, newConfig.CertFile)
|
||||
}
|
||||
if oldConfig.KeyFile != newConfig.KeyFile {
|
||||
log.Printf("key_file changed from %s to %s", oldConfig.KeyFile, newConfig.KeyFile)
|
||||
}
|
||||
|
||||
// Compare Routes
|
||||
for key := range oldConfig.Routes {
|
||||
if newVal, ok := newConfig.Routes[key]; !ok {
|
||||
log.Printf("Route %s removed (was %s)", key, oldConfig.Routes[key])
|
||||
} else if oldConfig.Routes[key] != newVal {
|
||||
log.Printf("Route %s changed from %s to %s", key, oldConfig.Routes[key], newVal)
|
||||
}
|
||||
}
|
||||
for key, newVal := range newConfig.Routes {
|
||||
if _, ok := oldConfig.Routes[key]; !ok {
|
||||
log.Printf("Route %s added: %s", key, newVal)
|
||||
}
|
||||
}
|
||||
|
||||
// Compare TrustTarget
|
||||
for key := range oldConfig.TrustTarget {
|
||||
if newVal, ok := newConfig.TrustTarget[key]; !ok {
|
||||
log.Printf("trust_target %s removed (was %t)", key, oldConfig.TrustTarget[key])
|
||||
} else if oldConfig.TrustTarget[key] != newVal {
|
||||
log.Printf("trust_target %s changed from %t to %t", key, oldConfig.TrustTarget[key], newVal)
|
||||
}
|
||||
}
|
||||
for key, newVal := range newConfig.TrustTarget {
|
||||
if _, ok := oldConfig.TrustTarget[key]; !ok {
|
||||
log.Printf("trust_target %s added: %t", key, newVal)
|
||||
}
|
||||
}
|
||||
|
||||
// Compare NoHTTPSRedirect
|
||||
for key := range oldConfig.NoHTTPSRedirect {
|
||||
if newVal, ok := newConfig.NoHTTPSRedirect[key]; !ok {
|
||||
log.Printf("no_https_redirect %s removed (was %t)", key, oldConfig.NoHTTPSRedirect[key])
|
||||
} else if oldConfig.NoHTTPSRedirect[key] != newVal {
|
||||
log.Printf("no_https_redirect %s changed from %t to %t", key, oldConfig.NoHTTPSRedirect[key], newVal)
|
||||
}
|
||||
}
|
||||
for key, newVal := range newConfig.NoHTTPSRedirect {
|
||||
if _, ok := oldConfig.NoHTTPSRedirect[key]; !ok {
|
||||
log.Printf("no_https_redirect %s added: %t", key, newVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reloadCert reloads the SSL certificate from disk
|
||||
func reloadCert(log *log.Logger) {
|
||||
cert, err := tls.LoadX509KeyPair(currentConfig.CertFile, currentConfig.KeyFile)
|
||||
if err != nil {
|
||||
log.Println("Error reloading cert:", err)
|
||||
return
|
||||
}
|
||||
certMutex.Lock()
|
||||
currentCert = &cert
|
||||
certMutex.Unlock()
|
||||
}
|
||||
|
||||
// updateCertWatchers updates the file watcher for new cert file paths
|
||||
func updateCertWatchers(log *log.Logger, oldCertFile, oldKeyFile string) {
|
||||
if oldCertFile != currentConfig.CertFile {
|
||||
watcher.Remove(oldCertFile)
|
||||
if err := watcher.Add(currentConfig.CertFile); err != nil {
|
||||
log.Println("Error watching new cert file:", err)
|
||||
}
|
||||
}
|
||||
if oldKeyFile != currentConfig.KeyFile {
|
||||
watcher.Remove(oldKeyFile)
|
||||
if err := watcher.Add(currentConfig.KeyFile); err != nil {
|
||||
log.Println("Error watching new key file:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
96
golangproxy/proxy/proxy.go
Normal file
96
golangproxy/proxy/proxy.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"golangproxy/logger"
|
||||
)
|
||||
|
||||
// Route holds proxy configuration for a specific host
|
||||
type Route struct {
|
||||
Proxy *httputil.ReverseProxy // The reverse proxy instance
|
||||
Handler http.Handler // Custom handler wrapping the proxy
|
||||
NoHTTPSRedirect bool // Disable HTTP to HTTPS redirect
|
||||
Target string // Target URL for proxying
|
||||
}
|
||||
|
||||
// CreateRoute initializes a reverse proxy for a target with trust settings
|
||||
func CreateRoute(target string, trustInvalidCert bool) *Route {
|
||||
url, _ := url.Parse(target)
|
||||
proxy := httputil.NewSingleHostReverseProxy(url)
|
||||
if url.Scheme == "https" {
|
||||
proxy.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: trustInvalidCert},
|
||||
}
|
||||
}
|
||||
|
||||
// Modify the Director based on whether the target is an IP or hostname
|
||||
originalDirector := proxy.Director
|
||||
proxy.Director = func(req *http.Request) {
|
||||
originalDirector(req)
|
||||
if isIPTarget(url.Hostname()) {
|
||||
// For IP targets, preserve the incoming Host header (e.g., main.example.com)
|
||||
// This ensures session cookies match the client's requested domain
|
||||
} else {
|
||||
// For hostname targets, set Host to the target's hostname (e.g., example.com)
|
||||
req.Host = url.Host
|
||||
}
|
||||
req.Header.Set("X-Forwarded-For", req.RemoteAddr)
|
||||
req.Header.Set("X-Forwarded-Host", req.Host)
|
||||
req.Header.Set("X-Forwarded-Proto", url.Scheme)
|
||||
if req.Header.Get("User-Agent") == "" {
|
||||
req.Header.Set("User-Agent", "GoLangProxy")
|
||||
}
|
||||
//logger.Logger.Printf("Proxying to %s - Headers: %v, Cookies: %v", target, req.Header, req.Cookies())
|
||||
}
|
||||
|
||||
// Create a custom handler to wrap the proxy and filter context canceled errors
|
||||
handler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
rwWrapper := &responseWriterWrapper{ResponseWriter: rw}
|
||||
proxy.ServeHTTP(rwWrapper, req)
|
||||
if err := req.Context().Err(); err != nil && err != context.Canceled {
|
||||
logger.Logger.Printf("Proxy error for %s: %v", target, err)
|
||||
}
|
||||
//logger.Logger.Printf("Response from %s - Headers: %v, Status: %d", target, rwWrapper.Header(), rwWrapper.status)
|
||||
})
|
||||
|
||||
return &Route{
|
||||
Proxy: proxy,
|
||||
Handler: handler,
|
||||
Target: target,
|
||||
}
|
||||
}
|
||||
|
||||
// isIPTarget checks if the target hostname is an IP address
|
||||
func isIPTarget(host string) bool {
|
||||
// Split host and port if a port is present (e.g., "10.100.111.254:4444")
|
||||
hostname, _, err := net.SplitHostPort(host)
|
||||
if err != nil {
|
||||
// If there's no port (or an error splitting), use the original host
|
||||
hostname = host
|
||||
}
|
||||
return net.ParseIP(hostname) != nil
|
||||
}
|
||||
|
||||
// responseWriterWrapper captures response status and headers
|
||||
type responseWriterWrapper struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (rw *responseWriterWrapper) WriteHeader(status int) {
|
||||
rw.status = status
|
||||
rw.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
func (rw *responseWriterWrapper) Write(b []byte) (int, error) {
|
||||
if rw.status == 0 {
|
||||
rw.status = http.StatusOK
|
||||
}
|
||||
return rw.ResponseWriter.Write(b)
|
||||
}
|
||||
39
golangproxy/server/server.go
Normal file
39
golangproxy/server/server.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// StartServer launches a web server on 127.0.0.1:61147
|
||||
func StartServer() {
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
indexPath := filepath.Join("www", "index.html")
|
||||
if _, err := os.Stat(indexPath); os.IsNotExist(err) {
|
||||
// Create index.html if it doesn’t exist
|
||||
if err := os.MkdirAll("www", 0755); err != nil {
|
||||
http.Error(w, "Error creating www directory", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
file, err := os.Create(indexPath)
|
||||
if err != nil {
|
||||
http.Error(w, "Error creating index.html", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = file.WriteString("<h1>GoLangProxy is up</h1>")
|
||||
if err != nil {
|
||||
http.Error(w, "Error writing index.html", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.ServeFile(w, r, indexPath)
|
||||
})
|
||||
|
||||
fmt.Println("Starting simple web server on 127.0.0.1:61147")
|
||||
if err := http.ListenAndServe("127.0.0.1:61147", nil); err != nil {
|
||||
fmt.Println("Web server error:", err)
|
||||
}
|
||||
}
|
||||
101
golangproxy/ssl/ssl.go
Normal file
101
golangproxy/ssl/ssl.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package ssl
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"golangproxy/logger"
|
||||
)
|
||||
|
||||
// EnsureCertFiles ensures SSL certificate and key files exist, generating self-signed if needed
|
||||
func EnsureCertFiles(certPath, keyPath string) error {
|
||||
_, certErr := os.Stat(certPath)
|
||||
_, keyErr := os.Stat(keyPath)
|
||||
if os.IsNotExist(certErr) || os.IsNotExist(keyErr) {
|
||||
logger.Logger.Printf("Certificate or key missing, generating new ones: %s, %s", certPath, keyPath)
|
||||
return generateSelfSignedCert(certPath, keyPath)
|
||||
}
|
||||
logger.Logger.Printf("Certificate and key found: %s, %s", certPath, keyPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateSelfSignedCert creates a self-signed certificate and key
|
||||
func generateSelfSignedCert(certPath, keyPath string) error {
|
||||
// Ensure ssl directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(certPath), 0755); err != nil {
|
||||
logger.Logger.Printf("Error creating ssl directory: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Logger.Println("Created ssl directory")
|
||||
|
||||
// Generate private key
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
logger.Logger.Printf("Error generating private key: %v", err)
|
||||
return err
|
||||
}
|
||||
if err := priv.Validate(); err != nil {
|
||||
logger.Logger.Printf("Generated private key is invalid: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Logger.Println("Generated and validated 2048-bit RSA private key")
|
||||
|
||||
// Create certificate template
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"GoLangProxy Self-Signed"},
|
||||
CommonName: "example.com",
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: []string{"example.com", "localhost"}, // SANs required
|
||||
}
|
||||
logger.Logger.Printf("Created certificate template with CN=%s, DNSNames=%v", template.Subject.CommonName, template.DNSNames)
|
||||
|
||||
// Generate certificate
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
logger.Logger.Printf("Error creating certificate: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Logger.Printf("Generated certificate, DER length: %d", len(certDER))
|
||||
|
||||
// Write certificate
|
||||
certOut, err := os.Create(certPath)
|
||||
if err != nil {
|
||||
logger.Logger.Printf("Error creating cert file %s: %v", certPath, err)
|
||||
return err
|
||||
}
|
||||
defer certOut.Close()
|
||||
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}); err != nil {
|
||||
logger.Logger.Printf("Error encoding cert PEM: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Logger.Printf("Wrote certificate to %s", certPath)
|
||||
|
||||
// Write private key
|
||||
keyOut, err := os.Create(keyPath)
|
||||
if err != nil {
|
||||
logger.Logger.Printf("Error creating key file %s: %v", keyPath, err)
|
||||
return err
|
||||
}
|
||||
defer keyOut.Close()
|
||||
if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
|
||||
logger.Logger.Printf("Error encoding key PEM: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Logger.Printf("Wrote private key to %s", keyPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
21
golangproxy/tests/config_test.go
Normal file
21
golangproxy/tests/config_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"golangproxy/config"
|
||||
)
|
||||
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
// Test loading existing config
|
||||
// Test creating default config
|
||||
os.Remove("test_config.yaml")
|
||||
config, err := config.LoadConfig("test_config.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("Error loading config: %v", err)
|
||||
}
|
||||
if config.ListenHTTP != ":80" {
|
||||
t.Errorf("Expected ListenHTTP :80, got %s", config.ListenHTTP)
|
||||
}
|
||||
}
|
||||
15
golangproxy/tests/proxy_test.go
Normal file
15
golangproxy/tests/proxy_test.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golangproxy/proxy"
|
||||
)
|
||||
|
||||
func TestCreateRoute(t *testing.T) {
|
||||
// Test HTTP target
|
||||
route := proxy.CreateRoute("http://example.com", false)
|
||||
if route.Target != "http://example.com" {
|
||||
t.Errorf("Expected target http://example.com, got %s", route.Target)
|
||||
}
|
||||
}
|
||||
9
golangproxy/tests/server_test.go
Normal file
9
golangproxy/tests/server_test.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStartServer(t *testing.T) {
|
||||
// Test requires mocking or running server in a goroutine
|
||||
}
|
||||
19
golangproxy/tests/ssl_test.go
Normal file
19
golangproxy/tests/ssl_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"golangproxy/ssl"
|
||||
)
|
||||
|
||||
func TestEnsureCertFiles(t *testing.T) {
|
||||
os.RemoveAll("ssl")
|
||||
err := ssl.EnsureCertFiles("ssl/cert.pem", "ssl/key.pem")
|
||||
if err != nil {
|
||||
t.Fatalf("Error generating certs: %v", err)
|
||||
}
|
||||
if _, err := os.Stat("ssl/cert.pem"); os.IsNotExist(err) {
|
||||
t.Error("Certificate file not created")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user