commit c9565782b4726b704000f37940eeffa83bba2d4d Author: dasha_uwu Date: Sun Aug 3 18:56:54 2025 +0500 initial commit diff --git a/config.go b/config.go new file mode 100644 index 0000000..ef70c73 --- /dev/null +++ b/config.go @@ -0,0 +1,108 @@ +package main + +import ( + "crypto/sha256" + "net/url" + "os" + + "github.com/goccy/go-yaml" +) + +type listenerDef struct { + protocol string + address string + acceptProxy bool +} + +var listenerDefs []listenerDef + +type credentials struct { + username []byte + password []byte +} + +type pathDef struct { + path string + targetURL *url.URL + credentials []credentials +} + +var hostDefs map[string][]pathDef + +func parseConfig(configFile string) { + configData, err := os.ReadFile(configFile) + if err != nil { + panic(err) + } + + var config struct { + Listeners []struct { + Protocol string + Address string + AcceptProxy bool + } + Hosts []struct { + Host string + Paths []struct { + Path string + Address string + Credentials []struct { + Username string + Password string + } + } + } + } + + err = yaml.Unmarshal(configData, &config) + if err != nil { + panic(err) + } + + hostDefs = make(map[string][]pathDef, len(config.Hosts)) + for _, host := range config.Hosts { + hostDef := make([]pathDef, len(host.Paths)) + hostDefs[host.Host] = hostDef + for i, path := range host.Paths { + targetURL, err := url.Parse(path.Address) + if err != nil { + panic(err) + } + + var pdCredentials []credentials + if path.Credentials != nil { + pdCredentials = make([]credentials, len(path.Credentials)) + for j, cfgCredentials := range path.Credentials { + hash := sha256.New() + hash.Write([]byte(cfgCredentials.Username)) + usernameSum := hash.Sum(nil) + + hash = sha256.New() + hash.Write([]byte(cfgCredentials.Password)) + passwordSum := hash.Sum(nil) + + pdCredentials[j] = credentials{ + username: usernameSum, + password: passwordSum, + } + } + } + + hostDef[i] = pathDef{ + path: path.Path, + targetURL: targetURL, + credentials: pdCredentials, + } + } + } + + listenerDefs = make([]listenerDef, len(config.Listeners)) + for i, listener := range config.Listeners { + listenerDef := listenerDef{ + protocol: listener.Protocol, + address: listener.Address, + acceptProxy: listener.AcceptProxy, + } + listenerDefs[i] = listenerDef + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4a76eff --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module git.linuxping.win/dgrp/v2 + +go 1.24.5 + +require ( + github.com/armon/go-proxyproto v0.1.0 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7c1ebb8 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/armon/go-proxyproto v0.1.0 h1:TWWcSsjco7o2itn6r25/5AqKBiWmsiuzsUDLT/MTl7k= +github.com/armon/go-proxyproto v0.1.0/go.mod h1:Xj90dce2VKbHzRAeiVQAMBtj4M5oidoXJ8lmgyW21mw= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= diff --git a/log.go b/log.go new file mode 100644 index 0000000..3904e89 --- /dev/null +++ b/log.go @@ -0,0 +1,49 @@ +package main + +import ( + "log" + "net/http" + "strings" + "time" +) + +type responseWriter struct { + http.ResponseWriter + status int + wroteHeader bool +} + +func (rw *responseWriter) WriteHeader(code int) { + if rw.wroteHeader { + return + } + + rw.status = code + rw.ResponseWriter.WriteHeader(code) + rw.wroteHeader = true + + return +} + +func loghttpreq(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + method := r.Method + path := r.URL.EscapedPath() + endpoint := strings.Split(r.RemoteAddr, ":") + ip := endpoint[0] + wrapped := &responseWriter{ + ResponseWriter: w, + } + start := time.Now() + next.ServeHTTP(wrapped, r) + responseTime := time.Since(start) + log.Printf( + "%-15v %14v %v %-7v %v", + ip, + responseTime, + wrapped.status, + method, + path, + ) + }) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..373f2ed --- /dev/null +++ b/main.go @@ -0,0 +1,139 @@ +package main + +import ( + "context" + "crypto/sha256" + "crypto/subtle" + "flag" + "net" + "net/http" + "net/http/httputil" + "net/url" + "strings" + "sync" + + "github.com/armon/go-proxyproto" +) + +func main() { + var configFile string + flag.StringVar(&configFile, "config", "config.yaml", "Config file") + flag.Parse() + + parseConfig(configFile) + + handler := loghttpreq(proxy()) + + wg := sync.WaitGroup{} + + for _, ld := range listenerDefs { + listener, err := net.Listen(ld.protocol, ld.address) + if err != nil { + panic(err) + } + + if ld.acceptProxy { + listener = &proxyproto.Listener{ + Listener: listener, + } + } + + wg.Add(1) + go func() { + http.Serve(listener, handler) + wg.Done() + }() + } + + wg.Wait() +} + +func rewrite(proxyRequest *httputil.ProxyRequest) { + target, ok := proxyRequest.In.Context().Value("target").(*url.URL) + if !ok { + panic("unreachable") + } + proxyRequest.SetURL(target) + proxyRequest.SetXForwarded() +} + +func proxy() http.Handler { + revproxy := &httputil.ReverseProxy{ + Rewrite: rewrite, + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tmp := strings.Split(r.Host, ":") + if len(tmp) == 0 { + w.WriteHeader(400) + return + } + hst := tmp[0] + + hd, ok := hostDefs[hst] + if !ok { + hd, ok = hostDefs[""] + if !ok { + w.WriteHeader(400) + return + } + } + + var pathDef pathDef + matched := false + for _, hostPathDef := range hd { + if !strings.HasPrefix(r.URL.Path, hostPathDef.path) { + continue + } + if !matched || len(hostPathDef.path) > len(pathDef.path) { + matched = true + pathDef = hostPathDef + } + } + + if !matched { + w.WriteHeader(400) + return + } + + r.URL.Path = r.URL.Path[len(pathDef.path):] + + if pathDef.credentials != nil { + match := false + + username, password, ok := r.BasicAuth() + if ok { + hash := sha256.New() + hash.Write([]byte(username)) + usernameSum := hash.Sum(nil) + + hash = sha256.New() + hash.Write([]byte(password)) + passwordSum := hash.Sum(nil) + + for _, cred := range pathDef.credentials { + res1 := subtle.ConstantTimeCompare(cred.username, usernameSum) == 1 + res2 := subtle.ConstantTimeCompare(cred.password, passwordSum) == 1 + if res1 && res2 { + match = true + break + } + } + } + + if !match { + w.Header().Set("WWW-Authenticate", "Basic") + w.WriteHeader(401) + return + } + } + + ctx := r.Context() + ctx = context.WithValue(ctx, "target", pathDef.targetURL) + r = r.WithContext(ctx) + + w.Header().Add("Server", "dgrp") + + revproxy.ServeHTTP(w, r) + }) +}