This commit is contained in:
Zeev Diukman 2025-03-02 16:14:03 +00:00
commit fbaf9393ea
18 changed files with 1855 additions and 0 deletions

52
.air.toml Normal file
View file

@ -0,0 +1,52 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd/server/."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata","docker"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html","yml","yaml"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
silent = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true

16
.env Normal file
View file

@ -0,0 +1,16 @@
# Keycloak openid provider configuration
KEYCLOAK_REALM=dev
KEYCLOAK_CLIENT_ID=dev_client
KEYCLOAK_CLIENT_SECRET=dWhSJgARBAuBAXN7sUTpqpIq2sKQdugs
KEYCLOAK_HOST_URL=http://192.168.10.2:8080
KEYCLOAK_REDIRECT_URI=https://app.z.com/auth/callback
# session configuration
SESSION_SECRET=dbemG9m84LmgdYLj4o_wai9Mz18QFHSNZeH92lgxytE
SECRET_KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwSuI+Iyxl4vUzFniKCJQfxAKzvx0wioUlPZc7YsFYGHQ9vIhTNI3kdSD75El6VYy3QSt1jHo/6fu1Oy5Brj95KFf496IQZ3gTLOpu3yVcB55r8nuO07o/9aOex4XItV9Gs9gqdTTq8p5uQrBH1ykq6fCDU57qLCWhijT04MN3DlgRTaNCY2h7XVxmiPORgz+JCAz4OcDM3Xq/ejWZToX+aphYVWIQRxU1mzyq9BuKZzU5tJIkVDQDhDZQyZNY61q4MHfqMKRUS6+5fJZbQWcgt3/4B+yUp/oVlmJjaEMuFDPyzZCHtm+r1Idw/ajMTzlwOFbnj6/8qteFIP/b9uWdQIDAQAB
# Auth configuration
AUTH_PREFIX=/auth
CALLBACK_PATH=/callback
LOGIN_PATH=/login
LOGOUT_PATH=/logout

14
cmd/server/functions.go Normal file
View file

@ -0,0 +1,14 @@
package main
import (
"crypto/tls"
)
// loadCertificate dynamically loads the certificate from files
func loadCertificate(certFile, keyFile string) (tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return tls.Certificate{}, err
}
return cert, nil
}

437
cmd/server/main.go Normal file
View file

@ -0,0 +1,437 @@
package main
import (
"context"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"io"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gookit/goutil/dump"
"zeevdiukman.com/zprox/internal/config"
"zeevdiukman.com/zprox/internal/logic"
"zeevdiukman.com/zprox/internal/reverse_proxy"
"zeevdiukman.com/zprox/internal/router"
"zeevdiukman.com/zprox/pkg/helper"
)
const DEVELOPMENT bool = true
type EntryPoints map[string]EntryPoint
type EntryPoint struct {
Name string
Group string
*http.Server
}
type ReverseProxies map[string]ReverseProxy
type ReverseProxy *httputil.ReverseProxy
var app = logic.NewApp()
func main() {
helper.AppRunner(func() {
config.Wrapper(func(c *config.Config) {
groups := logic.NewGroups()
mainRouter := router.New()
groups.ForEach(func(k string, g *logic.Group) {
groupSubRouter := mainRouter.Mux.NewRoute().Subrouter()
// groupSubRouter.Use(Domain)
for k := range g.ReverseProxies {
rpConfig := c.ReverseProxies[k]
domain := rpConfig.Domain
proxy := reverse_proxy.New(rpConfig.Host)
proxy.Name = domain
newRoute := groupSubRouter.NewRoute()
subRouter := newRoute.Host(domain).Subrouter()
if rpConfig.Auth != "" {
if _, ok := c.Auth[rpConfig.Auth]; !ok {
err := errors.New("Error: Auth " + rpConfig.Auth + " not exist!")
panic(err.Error())
}
pths := c.Auth[rpConfig.Auth].Paths
authRoute := subRouter.NewRoute()
subRouter.Use(Middleware_SetHeaders)
authSubRouter := authRoute.PathPrefix(pths.Prefix).Subrouter()
authSubRouter.Path(pths.Login).Handler(http.HandlerFunc(LoginHandler))
authSubRouter.Path(pths.Logout).Handler(http.HandlerFunc(LogoutHandler))
authSubRouter.Path(pths.Callback).Handler(http.HandlerFunc(CallbackHandler))
subRouter.Use(authMiddleware)
}
subRouter.PathPrefix("/").Handler(proxy.Httputil)
}
if len(g.ReverseProxies) > 0 {
tlsConfig := &tls.Config{
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
// crt, key := "", ""
crt, key := c.GetCertsPairByDomain(info.ServerName)
if crt == "" && key == "" {
// crt = c.TLS.Certs["default"].Cert
// key = c.TLS.Certs["default"].Key
// panic("Error: TLS cert and key not found!")
}
cert, err := loadCertificate(crt, key)
if err != nil {
return nil, err
}
return &cert, nil
},
}
server := &http.Server{
Addr: ":" + g.Port,
Handler: app.SessionManager.LoadAndSave(groupSubRouter),
TLSConfig: tlsConfig,
}
var err error
go func() {
ipAddr := helper.GetIP()
log.Println("Test server is running at http://" + ipAddr + ":" + g.Port)
if g.TLS {
err = server.ListenAndServeTLS("", "")
} else {
err = server.ListenAndServe()
}
if err != nil {
log.Println(err.Error())
}
}()
}
})
helper.StartTestHTTPServer(3000)
})
})
}
// //////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
config.Wrapper(func(c *config.Config) {
currentPath := r.URL.Path
authName := c.GetAuthNameByDomain(r.Host)
// authName := c.DataMaps.DomainToAuth[r.Host]
loginPath := c.Auth[authName].Paths.Prefix + c.Auth[authName].Paths.Login
logoutPath := c.Auth[authName].Paths.Prefix + c.Auth[authName].Paths.Logout
callbackPath := c.Auth[authName].Paths.Prefix + c.Auth[authName].Paths.Callback
// loginPath := c.Auth[authName].Paths.Prefix + c.Auth[authName].Paths.Login
// logoutPath := c.Auth[authName].Paths.Prefix + c.Auth[authName].Paths.Logout
// callbackPath := c.Auth[authName].Paths.Prefix + c.Auth[authName].Paths.Callback
// TODO: mark auth reverse proxy in yaml
// AuthHostUrl, _ := url.Parse(c.Auth.Default.OpenID.Host)
if r.Host == "keycloak.z.com" {
next.ServeHTTP(w, r)
}
switch currentPath {
case loginPath:
{
// fmt.Fprintln(w, "LOGIN")
next.ServeHTTP(w, r)
}
case logoutPath:
{
next.ServeHTTP(w, r)
// return
}
case callbackPath:
{
next.ServeHTTP(w, r)
// return
}
default:
{
accessToken := app.SessionManager.GetString(r.Context(), "access_token")
if accessToken == "" {
authName := c.DataMaps.DomainToAuth[r.Host]
http.Redirect(w, r, c.Auth[authName].Paths.Prefix+c.Auth[authName].Paths.Login, http.StatusFound)
return
}
// auth.SetAuthHeader(w, accessToken)
a := c.Auth[authName]
pths := a.Paths
prefix := pths.Prefix
login := pths.Login
logout := pths.Logout
loginPath := prefix + login
logoutPath := prefix + logout
if loginPath == r.URL.Path || logoutPath == r.URL.Path {
next.ServeHTTP(w, r)
// return
}
// tokenOk := IsAuthorizedJWT(accessToken, c, "default")
// if tokenOk {
// } else {
// // p := a.OpenID
// // Redirect to login
// }
next.ServeHTTP(w, r)
}
}
})
})
}
func Domain(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// c := config.New().Auth.Default.Paths
// requestedPath := r.URL.Path
// a := c
// excludedPaths := []string{
// a.Prefix + a.Login,
// a.Prefix + a.Callback,
// a.Prefix + a.Logout,
// }
// contains := helper.IsSliceContains(excludedPaths, requestedPath)
// if !contains {
// app.SessionManager.Put(r.Context(), "original_path", requestedPath)
// }
next.ServeHTTP(w, r)
})
}
func Middleware_SetHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Header.Set("X-Forwarded-Proto", getProto(r))
r.Header.Set("X-Forwarded-For", r.RemoteAddr)
r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Real-IP", r.RemoteAddr)
next.ServeHTTP(w, r)
})
}
func getProto(req *http.Request) string {
if req.TLS != nil {
return "https"
} else {
return "http"
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////
type TokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
RefreshExpiresIn int `json:"refresh_expires_in"`
TokenType string `json:"token_type"`
NotBeforePolicy int `json:"not-before-policy"`
SessionState string `json:"session_state"`
Scope string `json:"scope"`
RefreshToken string `json:"refresh_token"`
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
}
type Res401Struct struct {
Status string `json:"status" example:"FAILED"`
HTTPCode int `json:"httpCode" example:"401"`
Message string `json:"message" example:"authorisation failed"`
}
type Claims struct {
ResourceAccess client `json:"resource_access,omitempty"`
JTI string `json:"jti,omitempty"`
}
type client struct {
DemoServiceClient clientRoles `json:"DemoServiceClient,omitempty"`
}
type clientRoles struct {
Roles []string `json:"roles,omitempty"`
}
type HandlerFuncConfigWrapper func(*config.Config, http.ResponseWriter, *http.Request) *http.Handler
// HANDLERS
// ////////////
func CallbackHandler(w http.ResponseWriter, r *http.Request) {
config.Wrapper(func(c *config.Config) {
query := r.URL.Query()
code := query.Get("code")
state := query.Get("state")
verifier := app.SessionManager.GetString(r.Context(), "code_verifier")
if verifier == "" {
http.Error(w, "Code verifier not found in session", http.StatusBadRequest)
return
}
expectedState := app.SessionManager.GetString(r.Context(), "state")
if state != expectedState {
http.Error(w, "Invalid state parameter", http.StatusBadRequest)
return
}
originalPath := app.SessionManager.GetString(r.Context(), "original_path")
authName := c.GetAuthNameByDomain(r.Host)
token, fullResponse, e := exchangeCode(code, verifier, c, authName)
if e != nil {
dump.Println("exchangeCode: " + e.Error())
}
app.SessionManager.Put(r.Context(), "access_token", token.AccessToken)
app.SessionManager.Put(r.Context(), "full_token", fullResponse)
// SetAuthHeader(w, token.AccessToken)
http.Redirect(w, r, originalPath, http.StatusFound)
})
}
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
config.Wrapper(func(c *config.Config) {
app.SessionManager.Remove(r.Context(), "access_token")
app.SessionManager.Remove(r.Context(), "full_token")
authName := c.DataMaps.DomainToAuth[r.Host]
a := c.Auth[authName]
u := a.OpenID.EndPoints.Logout
http.Redirect(w, r, u, http.StatusFound)
})
}
func LoginHandler(w http.ResponseWriter, r *http.Request) {
config.Wrapper(func(c *config.Config) {
authName := c.DataMaps.DomainToAuth[r.Host]
codeVerifier, _ := generateCodeVerifier()
codeChallenge := generateCodeChallenge(codeVerifier)
state := helper.RandStringByBits(128)
nonce := helper.RandStringByBits(128)
authURL, _ := url.Parse(c.Auth[authName].OpenID.EndPoints.Auth)
query := authURL.Query()
query.Set("client_id", c.Auth[authName].OpenID.ClientID)
query.Set("response_type", "code")
query.Set("scope", "openid")
query.Set("redirect_uri", c.Auth[authName].OpenID.RedirectURI)
query.Set("code_challenge", codeChallenge)
query.Set("code_challenge_method", "S256")
query.Set("state", state)
query.Set("nonce", nonce)
authURL.RawQuery = query.Encode()
app.SessionManager.Put(r.Context(), "state", state)
app.SessionManager.Put(r.Context(), "code_verifier", codeVerifier)
http.Redirect(w, r, authURL.String(), http.StatusFound)
})
}
// AUTH FUNCTIONS
////////////////////
func exchangeCode(code string, verifier string, c *config.Config, authName string) (*TokenResponse, string, error) {
data := url.Values{}
data.Set("grant_type", "authorization_code")
data.Set("client_id", c.Auth[authName].OpenID.ClientID)
data.Set("client_secret", c.Auth[authName].OpenID.ClientSecert)
data.Set("redirect_uri", c.Auth[authName].OpenID.RedirectURI)
data.Set("code", code)
data.Set("scope", "openid zapp")
if verifier != "" {
data.Set("code_verifier", verifier)
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: DEVELOPMENT},
}
client := &http.Client{Transport: tr}
u := c.Auth[authName].OpenID.EndPoints.Token
r, _ := http.NewRequest(http.MethodPost, u, strings.NewReader(data.Encode()))
r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err := client.Do(r)
if err != nil {
dump.Println("ERROR exchange code: " + err.Error())
return nil, "", err
}
respBytes, err := io.ReadAll(resp.Body)
tokenResponse := &TokenResponse{}
json.Unmarshal(respBytes, &tokenResponse)
if err != nil {
dump.Println("ERROR exchange code Unmarshal: " + err.Error())
return nil, "", err
}
if tokenResponse.Error != "" {
dump.Println(tokenResponse.Error + ": " + tokenResponse.ErrorDescription)
}
fullResponse := string(respBytes)
return tokenResponse, fullResponse, nil
}
func generateCodeVerifier() (string, error) {
verifier := make([]byte, 32)
_, err := rand.Read(verifier)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(verifier), nil
}
func generateCodeChallenge(verifier string) string {
hash := sha256.Sum256([]byte(verifier))
return base64.RawURLEncoding.EncodeToString(hash[:])
}
func IsAuthorizedJWT(rawAccessToken string, c *config.Config, authName string) bool {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{
Timeout: time.Duration(6000) * time.Second,
Transport: tr,
}
ctx := oidc.ClientContext(context.Background(), client)
provider, err := oidc.NewProvider(ctx, c.Auth[authName].OpenID.EndPoints.Issuer)
if err != nil {
dump.Println("authorisation failed while getting the provider: " + err.Error())
return false
}
oidcConfig := &oidc.Config{
ClientID: c.Auth[authName].OpenID.ClientID,
}
verifier := provider.Verifier(oidcConfig)
idToken, err := verifier.Verify(ctx, rawAccessToken)
if err != nil {
dump.Println("authorisation failed while verifying the token: " + err.Error())
return false
}
var IDTokenClaims Claims // ID Token payload is just JSON.
if err := idToken.Claims(&IDTokenClaims); err != nil {
dump.Println("claims: " + err.Error())
return false
}
return true
}
///////////////////////////////////
///////////////////////////////////

81
config.yml Normal file
View file

@ -0,0 +1,81 @@
reverse_proxies:
kc:
domain: keycloak.z.com
host: http://127.0.0.1:8080
entry_point: https
tls:
enabled: true
certs: default
app:
domain: app.z.com
host: http://127.0.0.1:3000
entry_point: https
tls:
enabled: true
certs: default
auth: default
tls:
certs:
default:
cert: z.com.cert.pem
key: z.com.key.pem
entry_points:
https:
tls: true
port: 443
http:
port: 80
auth:
default:
paths:
prefix: /auth
login: /login
logout: /logout
callback: /callback
open_id:
host: http://127.0.0.1:8080
realm: dev
client_id: dev_client
client_secret: dWhSJgARBAuBAXN7sUTpqpIq2sKQdugs
redirect_uri: https://app.z.com/auth/callback
post_logout_redirect_uri: https://app.z.com/auth/logout
config_path: /realms/{{realm}}/.well-known/openid-configuration
# config_fields:
# - issuer
# - authorization_endpoint
# - token_endpoint
# - introspection_endpoint
# - userinfo_endpoint
# - end_session_endpoint
# - jwks_uri
# issuer: http://127.0.0.1:8080/realms/dev
# scope: openid profile email
# response_type: code
# response_mode: query
# prompt: none
# post_logout_redirect_uri: https://app.z.com/auth/logout
# token_endpoint_auth_method: client_secret_post
# userinfo_endpoint: https://keycloak.z.com/auth/realms/z/protocol/openid-connect/userinfo
# authorization_endpoint: https://keycloak.z.com/auth/realms/z/protocol/openid-connect/auth
# token_endpoint: https://keycloak.z.com/auth/realms/z/protocol/openid-connect/token
# end_session_endpoint: https://keycloak.z.com/auth/realms/z/protocol/openid-connect/logout
# jwks_uri: https://keycloak.z.com/auth/realms/z/protocol/openid-connect/certs
# issuer: https://keycloak.z.com/auth/realms/z
# registration_endpoint: https://keycloak.z.com/auth/realms/z/clients-registrations/openid-connect
# check_session_iframe: https://keycloak.z.com/auth/realms/z/protocol/openid-connect/login-status-iframe.html
# client_name: zapp
# client_uri: https://app.z.com
# logo_uri: https://app.z.com/logo.png
# policy_uri: https://app.z.com/policy
# tos_uri: https://app.z.com/tos
# jwks: https://keycloak.z.com/auth/realms/z/protocol/openid-connect/certs

38
go.mod Normal file
View file

@ -0,0 +1,38 @@
module zeevdiukman.com/zprox
go 1.24.0
require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
github.com/alexedwards/scs/v2 v2.8.0
github.com/coreos/go-oidc/v3 v3.12.0
github.com/gookit/goutil v0.6.18
github.com/gorilla/mux v1.8.1
github.com/spf13/viper v1.19.0
)

89
go.sum Normal file
View file

@ -0,0 +1,89 @@
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gookit/goutil v0.6.18 h1:MUVj0G16flubWT8zYVicIuisUiHdgirPAkmnfD2kKgw=
github.com/gookit/goutil v0.6.18/go.mod h1:AY/5sAwKe7Xck+mEbuxj0n/bc3qwrGNe3Oeulln7zBA=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

178
internal/config/config.go Normal file
View file

@ -0,0 +1,178 @@
package config
import (
"encoding/json"
"io"
"log"
"net/http"
"strings"
"github.com/spf13/viper"
"zeevdiukman.com/zprox/pkg/helper"
)
const Viper_File_Name string = "config"
const Viper_File_Type string = "yaml"
const Viper_File_Path string = "."
var c *Config
func init() {
helper.New().Screen.Clear()
c = createConfig()
}
func Wrapper(fn func(c *Config)) {
fn(c)
}
func Get() *Config {
return c
}
func createConfig() *Config {
c := &Config{}
c.initViper()
c.setViperOptions(Viper_File_Name, Viper_File_Type, Viper_File_Path)
c.viperReadYaml()
c.viperUnmarshalYaml()
c.initDomainToProxyNameMap()
c.initDomainToProxyAuthName()
c.initDomainToCertName()
c.initOpenIDEndPoints()
return c
}
func (c *Config) initViper() {
c.Viper = viper.New()
}
func (c *Config) setViperOptions(fileName string, fileExtention string, filePath string) {
c.Viper.SetConfigName(fileName)
c.Viper.SetConfigType(fileExtention)
c.Viper.AddConfigPath(filePath)
}
func (c *Config) viperReadYaml() {
err := c.Viper.ReadInConfig()
if err != nil {
log.Fatalf("Error reading config file, %s", err)
}
}
func (c *Config) viperUnmarshalYaml() {
err := c.Viper.Unmarshal(&c)
if err != nil {
log.Fatalf("Unable to decode into struct, %v", err)
}
}
func (eps *EntryPoints) ForEach(fn func(epName string, epConfig *EntryPoint)) {
for epName, epConfig := range *eps {
fn(epName, &epConfig)
}
}
func (rp *ReverseProxies) ForEach(fn func(rpName string, rpConfig *ReverseProxy)) {
for rpName, rpData := range *rp {
fn(rpName, &rpData)
}
}
func (c *Config) fetchEndPointsByAuthName(authName string) map[string]any {
configURL := c.KeycloakWellknownURL(authName)
resp := helper.IsFetchOK[EndPoints](configURL, "", http.Get)
respByes, err := io.ReadAll(resp.Body)
if err != nil {
log.Println(err.Error())
}
data := map[string]any{}
json.Unmarshal(respByes, &data)
return data
}
func (c *Config) initOpenIDEndPoints() {
for authName := range c.Auth {
data := c.fetchEndPointsByAuthName(authName)
c.Auth[authName].OpenID.EndPoints = &EndPoints{
Issuer: data["issuer"].(string),
Auth: data["authorization_endpoint"].(string),
Introspection: data["introspection_endpoint"].(string),
Token: data["token_endpoint"].(string),
UserInfo: data["userinfo_endpoint"].(string),
Logout: data["end_session_endpoint"].(string),
JwksUri: data["jwks_uri"].(string),
}
}
}
func (c *Config) KeycloakWellknownURL(authName string) string {
if _, ok := c.Auth[authName]; !ok {
return ""
}
hostUrl := c.Auth[authName].OpenID.Host
realm := c.Auth[authName].OpenID.Realm
configPath := c.Auth[authName].OpenID.ConfigPath
u := hostUrl
u += strings.ReplaceAll(configPath, "{{realm}}", realm)
return u
}
func (c *Config) initDomainToProxyNameMap() {
mp := make(map[string]string)
c.ReverseProxies.ForEach(func(rpName string, rpConfig *ReverseProxy) {
mp[rpConfig.Domain] = rpName
})
c.DataMaps.DomainToProxy = mp
}
func (c *Config) initDomainToProxyAuthName() {
authMap := map[string]int{}
for authName := range c.Auth {
authMap[authName] = 1
}
mp := make(map[string]string)
c.ReverseProxies.ForEach(func(rpName string, rpConfig *ReverseProxy) {
if v, ok := authMap[rpConfig.Auth]; ok && v == 1 {
mp[rpConfig.Domain] = rpConfig.Auth
}
})
c.DataMaps.DomainToAuth = mp
}
func (c *Config) initDomainToCertName() {
crtMap := map[string]int{}
for crtName := range c.TLS.Certs {
crtMap[crtName] = 1
}
mp := make(map[string]string)
c.ReverseProxies.ForEach(func(rpName string, rpConfig *ReverseProxy) {
if v, ok := crtMap[rpConfig.TLS.Certs]; ok && v == 1 {
mp[rpConfig.Domain] = rpConfig.TLS.Certs
}
mp[rpConfig.Domain] = rpConfig.TLS.Certs
})
c.DataMaps.DomainToCert = mp
}
func (c *Config) GetAuthNameByDomain(domain string) string {
return c.DataMaps.DomainToAuth[domain]
}
func (c *Config) GetProxyNameByDomain(domain string) string {
return c.DataMaps.DomainToProxy[domain]
}
func (c *Config) GetCertNameByDomain(domain string) string {
return c.DataMaps.DomainToCert[domain]
}
func (c *Config) GetCertsPairByDomain(domain string) (string, string) {
var crt string
var key string
certName := c.DataMaps.DomainToCert[domain]
crt = c.TLS.Certs[certName].Cert
key = c.TLS.Certs[certName].Key
if !(certName != "" && crt == "" && key != "") {
certName = "default"
crt = c.TLS.Certs[certName].Cert
key = c.TLS.Certs[certName].Key
}
return crt, key
}

93
internal/config/types.go Normal file
View file

@ -0,0 +1,93 @@
package config
import "github.com/spf13/viper"
type Config struct {
Viper *viper.Viper
DataMaps DataMaps
ReverseProxies ReverseProxies `mapstructure:"reverse_proxies"`
TLS TLS `mapstructure:"tls"`
EntryPoints EntryPoints `mapstructure:"entry_points"`
Auth map[string]*AuthInstance `mapstructure:"auth"`
}
type DataMaps struct {
DomainToProxy map[string]string
DomainToAuth map[string]string
DomainToCert map[string]string
}
// AUTH
// type Auth map[string]AuthInstance
type AuthInstance struct {
Paths Paths `mapstructure:"paths"`
OpenID OpenID `mapstructure:"open_id"`
}
type Paths struct {
Prefix string `mapstructure:"prefix"`
Login string `mapstructure:"login"`
Logout string `mapstructure:"logout"`
Callback string `mapstructure:"callback"`
}
type OpenID struct {
Host string `mapstructure:"host"`
Realm string `mapstructure:"realm"`
ClientID string `mapstructure:"client_id"`
ClientSecert string `mapstructure:"client_secret"`
RedirectURI string `mapstructure:"redirect_uri"`
PostLogoutRedirectURI string `mapstructure:"post_logout_redirect_uri"`
ConfigPath string `mapstructure:"config_path"`
EndPoints *EndPoints
}
type EndPoints struct {
Issuer string
Auth string
Introspection string
Token string
UserInfo string
Logout string
JwksUri string
}
// type OpenIdEndPoints struct {
// Issuer string
// Authorization string
// Token string
// Introspection string
// UserInfo string
// EndSession string
// }
// ReverseProxies
type ReverseProxies map[string]ReverseProxy
type ReverseProxy struct {
Domain string `mapstructure:"domain"`
Host string `mapstructure:"host"`
EntryPoint string `mapstructure:"entry_point"`
TLS TLS_RP `mapstructure:"tls"`
Auth string `mapstructure:"auth"`
}
type TLS_RP struct {
Enabled bool `mapstructure:"enabled"`
Certs string `mapstructure:"certs"`
}
// TLS
type TLS struct {
Certs map[string]Certs `mapstructure:"certs"`
}
type Certs struct {
Cert string `mapstructure:"cert"`
Key string `mapstructure:"key"`
}
// EntryPoints
type EntryPoints map[string]EntryPoint
type EntryPoint struct {
Port string `mapstructure:"port"`
TLS bool `mapstructure:"tls"`
}

99
internal/logic/logic.go Normal file
View file

@ -0,0 +1,99 @@
package logic
import (
"github.com/alexedwards/scs/v2"
conf "zeevdiukman.com/zprox/internal/config"
)
var config = conf.Get()
type App struct {
SessionManager *scs.SessionManager
}
func (app *App) Get() *App {
return app
}
func NewApp() *App {
app := &App{
SessionManager: scs.New(),
}
return app
}
type Groups map[string]*Group
type Group struct {
Port string
TLS bool
// ReverseProxies []string
ReverseProxies ReverseProxies
// Server string
// GroupRouter string
}
type ReverseProxies map[string]ReverseProxy
type ReverseProxy struct {
Certs string
}
func NewGroups() *Groups {
grps := &Groups{}
grps.initGroups()
return grps
}
func (grps Groups) ForEach(fn func(k string, g *Group)) {
for k, g := range grps {
fn(k, g)
}
}
func (rpxs ReverseProxies) Get() ReverseProxies {
n := make(ReverseProxies)
for k, v := range rpxs {
n[k] = v
}
return n
}
func (rpxs ReverseProxies) Set(key string, rp ReverseProxy) ReverseProxies {
newProxies := rpxs.Get()
newProxies[key] = rp
return newProxies
}
// func (rpxs ReverseProxy) Set(newRp *ReverseProxy) {
// rpxs = *newRp
// }
func (grps Groups) initGroups() {
config.EntryPoints.ForEach(func(epName string, epConfig *conf.EntryPoint) {
if _, ok := grps[epName]; !ok {
grps[epName] = &Group{}
}
grps[epName].TLS = epConfig.TLS
grps[epName].Port = epConfig.Port
})
config.ReverseProxies.ForEach(func(rpNameA string, rpConfigA *conf.ReverseProxy) {
rps := grps[rpConfigA.EntryPoint].ReverseProxies
certName := rpConfigA.TLS.Certs
// if _, ok := rps[rpNameA]; !ok {
// // grps[rpConfigA.EntryPoint] = &Group{}
// // rps[rpNameA]
// }
rp := ReverseProxy{Certs: certName}
a := insertValue(rps, rpNameA, rp)
grps[rpConfigA.EntryPoint].ReverseProxies = a
})
}
func insertValue[T any](mp map[string]T, key string, val T) map[string]T {
n := make(map[string]T)
for k, v := range mp {
n[k] = v
}
n[key] = val
mp = n
return mp
// grps[rpConfigA.EntryPoint].ReverseProxies = a
}

View file

@ -0,0 +1,53 @@
package middleware
// import (
// "net/http"
// "github.com/alexedwards/scs/v2"
// conf "zeevdiukman.com/zprox/internal/config"
// )
// // import (
// // "net/http"
// // )
// func Auth(c *conf.Config, sessionManager *scs.SessionManager) func(http.Handler) http.Handler {
// return func(next http.Handler) http.Handler {
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// accessToken := sessionManager.GetString(r.Context(), "access_token")
// // auth.SetAuthHeader(w, accessToken)
// loginPath := c.Auth.Prefix + c.Auth.LoginPath
// logoutPath := c.Auth.Prefix + c.Auth.LogoutPath
// if loginPath == r.URL.Path || logoutPath == r.URL.Path {
// next.ServeHTTP(w, r)
// return
// }
// tokenOk := Domain.IsAuthorizedJWT(accessToken)
// if tokenOk {
// next.ServeHTTP(w, r)
// } else {
// p := c.Auth
// http.Redirect(w, r, p.Prefix+p.LoginPath, http.StatusFound) // Redirect to login
// }
// })
// }
// }
// func Domain(next http.Handler) http.Handler {
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// // requestedPath := r.URL.Path
// // a := c.Auth
// // excludedPaths := []string{
// // a.Prefix + a.LoginPath,
// // a.Prefix + a.CallbackPath,
// // a.Prefix + a.LogoutPath,
// // }
// // contains := helper.IsSliceContains(excludedPaths, requestedPath)
// // if !contains {
// // sessionManager.Put(r.Context(), "original_path", requestedPath)
// // }
// next.ServeHTTP(w, r)
// })
// }

View file

@ -0,0 +1,107 @@
package reverse_proxy
import (
"net/http"
"net/http/httputil"
"net/url"
"strings"
conf "zeevdiukman.com/zprox/internal/config"
)
// var config = conf.New()
type ReverseProxy struct {
Name string
Host string
Httputil *httputil.ReverseProxy
}
func New(host string) *ReverseProxy {
return &ReverseProxy{
Host: host,
Httputil: &httputil.ReverseProxy{
Director: DefaultDirector(host),
},
}
}
type Data struct {
Name string
Value any
}
type DirectorFunc func(req *http.Request, data []Data) (*http.Request, []Data)
func GetData[T any](key string, data []Data) (res T) {
for _, v := range data {
if v.Name == key {
res = v.Value.(T)
}
}
return res
}
func (rp *ReverseProxy) DefaultDirectorFunc(d []Data, fn DirectorFunc) func(*http.Request) {
return func(r *http.Request) {
host := ""
req, data := fn(r, d)
proxyData := GetData[conf.ReverseProxy]("proxy_data", data)
host = proxyData.Host
target, _ := url.Parse(host)
targetQuery := target.RawQuery
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
}
}
func DefaultDirector(host string) func(*http.Request) {
return func(req *http.Request) {
target, _ := url.Parse(host)
targetQuery := target.RawQuery
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
}
}
func joinURLPath(a, b *url.URL) (path, rawpath string) {
if a.RawPath == "" && b.RawPath == "" {
return SingleJoiningSlash(a.Path, b.Path), ""
}
// Same as singleJoiningSlash, but uses EscapedPath to determine
// whether a slash should be added
apath := a.EscapedPath()
bpath := b.EscapedPath()
aslash := strings.HasSuffix(apath, "/")
bslash := strings.HasPrefix(bpath, "/")
switch {
case aslash && bslash:
return a.Path + b.Path[1:], apath + bpath[1:]
case !aslash && !bslash:
return a.Path + "/" + b.Path, apath + "/" + bpath
}
return a.Path + b.Path, apath + bpath
}
func SingleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}

24
internal/router/router.go Normal file
View file

@ -0,0 +1,24 @@
package router
import "github.com/gorilla/mux"
func New() *MainRouter {
r := &MainRouter{}
r.SetMux()
return r
}
func (r *MainRouter) SetMux() {
r.Mux = mux.NewRouter()
}
type MainRouter struct {
Subrouters Subrouters
Mux *mux.Router
}
type Subrouters map[string]SubRouter
type SubRouter struct {
Name string
Group string
*mux.Router
}

512
pkg/helper/helper.go Normal file
View file

@ -0,0 +1,512 @@
package helper
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"time"
"unicode"
"github.com/gookit/goutil/dump"
"github.com/gorilla/mux"
)
// const (
// COLOR_Reset = "\033[0m"
// COLOR_Red = "\033[31m"
// COLOR_Green = "\033[32m"
// COLOR_Yellow = "\033[33m"
// COLOR_Blue = "\033[34m"
// COLOR_Purple = "\033[35m"
// COLOR_Cyan = "\033[36m"
// COLOR_Gray = "\033[37m"
// COLOR_White = "\033[97m" // Brighter white
// )
// func Log(msg string) {
// log.New(os.Stdout, msg, log.Ldate|log.Ltime)
// }
// func Colorize(colorCode string, message string) string {
// return colorCode + message + COLOR_Reset
// }
type IsStruct struct{}
var (
Is IsStruct
h = New()
)
type Helper struct {
Convert Convert
Struct Struct
Screen Screen
Error Error
If If
Log Log
}
type Convert struct {
}
type Struct struct {
}
type Screen struct {
}
type Error struct {
Val error
}
type If struct {
Error Error
}
type Log struct {
Error Error
}
func (h *Helper) P(val any) {
dump.Println(val)
}
// InsertStringValueIntoField inserts a string value into a specified field of a struct.
//
// structPtr must be a pointer to a struct.
// fieldName is the name of the field (case-sensitive).
// stringValue is the string value to be inserted.
//
// Returns an error if:
// - structPtr is not a pointer to a struct.
// - The fieldName is not found in the struct.
// - The field is not settable (e.g., unexported).
// - The field is not of type string.
func New() *Helper {
return &Helper{}
}
func (e *Error) Log() {
if e.Val != nil {
log.Println(e.Val.Error())
}
}
func (e *Error) In(err error) *Error {
e.Val = err
return e
}
func (conv *Struct) Insert(structPtr interface{}, param ...any) {
// func (conv *Struct) Insert(structPtr interface{}, fieldName string, stringValue string, sep string) string {
rawFieldName := param[0].(string)
fieldValue := param[1]
sep := ""
if len(param) > 2 {
sep = param[2].(string)
} else {
sep = "_"
}
// if len(param[2].(string)) > 0 {
// sep = param[2].(string)
// }
seperatorRune := []rune(sep)[0]
field := CapitalizeAfterChar(rawFieldName, seperatorRune)
field = CapitalizeFirstLetter(field)
field = strings.ReplaceAll(field, sep, "")
err := InsertAnyValueIntoField(structPtr, field, fieldValue)
h.Error.Val = err
h.If.Error.Log()
}
func (conv *Struct) ToStructField(str string, seperator string) string {
seperatorRune := []rune(seperator)[0]
field := CapitalizeAfterChar(str, seperatorRune)
field = CapitalizeFirstLetter(field)
field = strings.ReplaceAll(field, seperator, "")
return field
}
func InsertStringValueIntoField(structPtr interface{}, fieldName string, stringValue string) error {
if structPtr == nil {
return errors.New("structPtr cannot be nil")
}
val := reflect.ValueOf(structPtr)
if val.Kind() != reflect.Ptr {
return errors.New("structPtr must be a pointer to a struct")
}
elem := val.Elem()
if elem.Kind() != reflect.Struct {
return errors.New("structPtr must be a pointer to a struct")
}
fieldVal := elem.FieldByName(fieldName)
if !fieldVal.IsValid() {
return fmt.Errorf("field '%s' not found in struct", fieldName)
}
if !fieldVal.CanSet() {
return fmt.Errorf("field '%s' is not settable (unexported or embedded without being exported)", fieldName)
}
if fieldVal.Kind() != reflect.String {
return fmt.Errorf("field '%s' is not a string type", fieldName)
}
fieldVal.SetString(stringValue)
return nil
}
func InsertAnyValueIntoField(structPtr interface{}, fieldName string, anyValue any) error {
if structPtr == nil {
return errors.New("structPtr cannot be nil")
}
val := reflect.ValueOf(structPtr)
if val.Kind() != reflect.Ptr {
return errors.New("structPtr must be a pointer to a struct")
}
elem := val.Elem()
if elem.Kind() != reflect.Struct {
return errors.New("structPtr must be a pointer to a struct")
}
fieldVal := elem.FieldByName(fieldName)
if !fieldVal.IsValid() {
return fmt.Errorf("field '%s' not found in struct", fieldName)
}
if !fieldVal.CanSet() {
return fmt.Errorf("field '%s' is not settable (unexported or embedded without being exported)", fieldName)
}
switch v := anyValue.(type) {
case string:
{
if fieldVal.Kind() != reflect.String {
return fmt.Errorf("field '%s' is not a string type", fieldName)
}
fieldVal.SetString(v)
return nil
}
case bool:
{
if fieldVal.Kind() != reflect.Bool {
return fmt.Errorf("field '%s' is not a string type", fieldName)
}
fieldVal.SetBool(v)
return nil
}
}
return fmt.Errorf("value of field '%s' is unknown type", fieldName)
}
// CapitalizeAfterChar capitalizes the first letter immediately following each occurrence of a specific character in a string.
//
// For example:
// CapitalizeAfterChar("hello_world", '_') == "hello_World"
// CapitalizeAfterChar("this-is-a-test", '-') == "this-Is-A-Test"
// CapitalizeAfterChar(" leading spaces and_underscores", '_') == " leading spaces and_Underscores"
func CapitalizeAfterChar(input string, char rune) string {
var result strings.Builder
capitalizeNext := false // Flag to indicate if the next letter should be capitalized
for _, r := range input {
if capitalizeNext {
result.WriteRune(unicode.ToUpper(r)) // Capitalize the current rune
capitalizeNext = false // Reset the flag
} else {
result.WriteRune(r) // Write the rune as is
}
if r == char {
capitalizeNext = true // Set the flag to capitalize the next letter
}
}
return result.String()
}
func CapitalizeAfterCharMulti(input string, char rune) string {
var result strings.Builder
capitalizeNext := false // Flag to indicate if the next letter should be capitalized
for _, r := range input {
if capitalizeNext {
result.WriteRune(unicode.ToUpper(r)) // Capitalize the current rune
capitalizeNext = false // Reset the flag
} else {
result.WriteRune(r) // Write the rune as is
}
if r == char {
capitalizeNext = true // Set the flag to capitalize the next letter
}
}
return result.String()
}
func CapitalizeFirstLetter(input string) string {
if input == "" {
return input // Return empty string if input is empty
}
runes := []rune(input) // Convert string to rune slice for Unicode support
firstRune := runes[0]
if !unicode.IsLetter(firstRune) {
return input // Return original string if first char is not a letter
}
capitalizedFirstRune := unicode.ToUpper(firstRune)
runes[0] = capitalizedFirstRune
return string(runes) // Convert rune slice back to string
}
func MapIter[K comparable, V comparable](m map[K]V, fn func(K, V)) {
for p, d := range m {
fn(p, d)
}
}
func (*Screen) Clear() {
if runtime.GOOS == "windows" {
cmd := exec.Command("cmd", "/c", "cls")
cmd.Stdout = os.Stdout
cmd.Run()
} else {
cmd := exec.Command("clear")
cmd.Stdout = os.Stdout
cmd.Run()
}
}
func RandStringByBits(nBits int) string {
b := make([]byte, nBits/8)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return err.Error()
}
return base64.RawURLEncoding.EncodeToString(b)
}
func StructToMap(obj interface{}) map[string]any {
val := reflect.ValueOf(obj)
typ := reflect.TypeOf(obj)
result := make(map[string]any)
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
result[field.Name] = val.Field(i).Interface()
}
return result
}
func FetchGetJson(u string) (map[string]any, error) {
resp, err := http.Get(u)
if err != nil {
return nil, err
}
respByes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
data := map[string]any{}
json.Unmarshal(respByes, &data)
return data, nil
}
func GetLastChar(str string) string {
return str[len(str)-1:]
}
func IsLastChar(str string, chr string) bool {
if l := GetLastChar(str); l == chr {
return true
}
return false
}
func IsLastCharDo(str string, chr string, f func(r bool)) bool {
result := IsLastChar(str, chr)
func(bool) {
f(result)
}(result)
return result
}
func IsFetchOK[V any](u string, funcName string, f func(string) (*http.Response, error)) *http.Response {
// errCntr := 0
errCntrTotal := 0
var (
a V
resp *http.Response
err error
)
for {
resp, err = f(u)
if err == nil && resp.StatusCode == 200 {
// ClearScreen()
break
} else {
// errCntr++
// errCntrTotal++
// if errCntr == 10 {
// errCntr = 0
// ClearScreen()
// }
// if errCntr == 1 {
// ClearScreen()
// }
errCntrTotalStr := strconv.Itoa(errCntrTotal)
pkg := reflect.TypeOf(a).PkgPath()
dt := time.Now()
dateTime := dt.Format("02-01-2006 15:04:05")
fmt.Println("ERROR: " + dateTime + ">---------<" + errCntrTotalStr + ">")
fmt.Println(" :")
fmt.Println(" Package name: " + pkg)
fmt.Println("Function name: " + funcName)
fmt.Println(" URL: " + u)
fmt.Println("Error message: " + err.Error())
fmt.Println("--------------------------------------")
time.Sleep(5 * time.Second)
}
}
return resp
}
func IsSliceContains[T comparable](sliceVals []T, valueToSearch T) bool {
for _, v := range sliceVals {
if v == valueToSearch {
return true
}
}
return false
}
func AddDotBetween(k ...string) string {
constructedKey := ""
for i, key := range k {
constructedKey += key
if i < len(k)-1 {
constructedKey += "."
}
}
return constructedKey
}
func RemoveSliceDuplicates(elements []string) []string {
encountered := map[string]bool{}
result := []string{}
for v := range elements {
if encountered[elements[v]] {
// Do not add duplicate.
} else {
// Record this element as an encountered element.
encountered[elements[v]] = true
// Append to result slice.
result = append(result, elements[v])
}
}
// Return the new slice.
return result
}
func IfLast[T any](counter int, someVar map[string]T, fn func()) {
counter++
if len(someVar) == counter {
fn()
}
}
func (s *IsStruct) Pointer(v any) bool {
valueOfP := reflect.ValueOf(v)
if valueOfP.Kind() == reflect.Ptr {
return true
} else {
return false
}
}
func AppRunner(runApp func()) {
wg := sync.WaitGroup{}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
runApp()
<-ctx.Done()
//do stuff after ending
wg.Wait()
fmt.Println("BYE BYE!")
os.Exit(0)
}
func StartTestHTTPServer(port int) {
p := strconv.Itoa(port)
go func() {
log.Println("Test server is running at http://" + GetIP() + ":" + p)
r := mux.NewRouter()
r.Path("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "/ OK")
})
r.Path("/test1").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "/test1 OK")
})
r.Path("/test2").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "/test2 OK")
})
err := http.ListenAndServe(":3000", r)
if err != nil {
log.Println(err.Error())
}
}()
}
func GetIP(prefix ...string) string {
prfx := "192.168."
if len(prefix) > 0 {
prfx = prefix[0]
}
addrs, err := net.InterfaceAddrs()
if err != nil {
fmt.Println("Error getting interface addresses:", err)
return ""
}
for _, addr := range addrs {
// Check if the address is an IP address
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
a := strings.HasPrefix(ipnet.IP.String(), prfx)
c := ipnet.IP.To4() != nil
if c && a { //check for ipv4
prfx = ipnet.IP.String()
break
}
// } else if ipnet.IP.To16() != nil { //check for ipv6
// fmt.Println("Local IPv6 address:", ipnet.IP.String())
// }
}
}
return prfx
}

1
tmp/build-errors.log Normal file

File diff suppressed because one or more lines are too long

BIN
tmp/main Executable file

Binary file not shown.

33
z.com.cert.pem Normal file
View file

@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
MIIFtzCCA5+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcTELMAkGA1UEBhMCSUwx
DjAMBgNVBAgMBUhhaWZhMQ4wDAYDVQQHDAVIYWlmYTEKMAgGA1UECgwBWjEKMAgG
A1UECwwBWjEOMAwGA1UEAwwFei5jb20xGjAYBgkqhkiG9w0BCQEWC2FkbWluQHou
Y29tMB4XDTI1MDIxNjE0NTQ1NloXDTI2MDIyNjE0NTQ1NlowYTELMAkGA1UEBhMC
SUwxDjAMBgNVBAgMBUhhaWZhMQowCAYDVQQKDAFaMQowCAYDVQQLDAFaMQ4wDAYD
VQQDDAV6LmNvbTEaMBgGCSqGSIb3DQEJARYLYWRtaW5Aei5jb20wggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpdNQlUMd1VOeXLYx5bLct+CCEC1XQDboD
DnhQcCOryNGYvOd+u/KyOIKie+TvMCzKSqtv66cy216r4xt1T0w/S5YGhthtM6UA
KZroRRqjuyxt9n+F+u8Mq6wuAYOitWYFSwDlLj+XUM++REljekgYMPJFjRNeshdv
ciAvmgSuba887lcfK9SnkV2GMGetqwtbSxBPPH2nsNl+1yPNrzJw1HatcZiyEF2e
UeseR+yJ8IBqwzalB+GdlPOiy31eKArUYn6F3mWrPpTuJKf8/uAEx4tHkEdeAiaP
lPH8/D2ACExi3bPQBZ0Mu4XlQujSG+vyW2UE6uiMAk+j4xEqCvTVAgMBAAGjggFn
MIIBYzAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIGQDAzBglghkgBhvhCAQ0E
JhYkT3BlblNTTCBHZW5lcmF0ZWQgU2VydmVyIENlcnRpZmljYXRlMB0GA1UdDgQW
BBRoAJ51iApg+pFjY16jj6Nutnw5+jCBrgYDVR0jBIGmMIGjgBSHMJSKqW4qORmC
/Wq8UImiAtuWjaF1pHMwcTELMAkGA1UEBhMCSUwxDjAMBgNVBAgMBUhhaWZhMQ4w
DAYDVQQHDAVIYWlmYTEKMAgGA1UECgwBWjEKMAgGA1UECwwBWjEOMAwGA1UEAwwF
ei5jb20xGjAYBgkqhkiG9w0BCQEWC2FkbWluQHouY29tghQD85U1CPpeLaY/YPkc
xQBVyuU4+zAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGQYD
VR0RBBIwEIIFei5jb22CByouei5jb20wDQYJKoZIhvcNAQELBQADggIBAF/IKfK8
dF6qAM0SMr+3K9fEQgJWDfHI2bLFnsFpzRHc5XUGnvp5sRCEXFDJIJSOwGQqv3rm
ylcFVBE4lawtMC/NpMMRSz7/e/NdA/b5CFtCK2EjnM/KYE9EV06EebP0u8yRIppM
Go71j7fAbncVmUwnhLcgIkpb+VKTfexFUIqSVgeXTFkIQa7ndP70W+AUt/5X+Rhy
7c3rE6fklTabMJR8SxQKLz6KJzSJnRLH75H7CxmF2d7NP23HuEfk7OrTRQP+ASmD
VJkzS6GtVQ/MEjVbj9ygABDVmk+0z2prJz1USyqniUhnznQZPOGz0F/M3VUiodsp
rW3YOyPh6Ze8bHih4ivuNTsiXQKSiWWgIP8zEu664hwWtuXZAbVKP3XfLZ5X7gJ2
Vqj9ulkIa+3VjSv+WA45QZVdBtSoOaNobRaKtjFnK5DWeW6t0e1+DG5PVZe1JTwH
DUxfUEnXDBfsfttqHAHCamWo1dpWzJB9lnTjXHwQHkYYEimTLrPVhzwFV5yegqkP
feMtsgbEsO+QqVSeqx3oy+W2J9tjBwAxrMg1TOMBSWRsaUvwtXwx1cf1bTzjhg9Y
2+dzTmITfxh3tWMh0jYmO1C3PH2K8HKraAXdqrGDbxK94iYpgA5bk48s2H1YCDcF
baYw2irjJC2cmsdGp3an0Mtb8sY4zLbGuRQl
-----END CERTIFICATE-----

28
z.com.key.pem Normal file
View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCpdNQlUMd1VOeX
LYx5bLct+CCEC1XQDboDDnhQcCOryNGYvOd+u/KyOIKie+TvMCzKSqtv66cy216r
4xt1T0w/S5YGhthtM6UAKZroRRqjuyxt9n+F+u8Mq6wuAYOitWYFSwDlLj+XUM++
REljekgYMPJFjRNeshdvciAvmgSuba887lcfK9SnkV2GMGetqwtbSxBPPH2nsNl+
1yPNrzJw1HatcZiyEF2eUeseR+yJ8IBqwzalB+GdlPOiy31eKArUYn6F3mWrPpTu
JKf8/uAEx4tHkEdeAiaPlPH8/D2ACExi3bPQBZ0Mu4XlQujSG+vyW2UE6uiMAk+j
4xEqCvTVAgMBAAECggEALwfSvUyPHxxibk3g9+5ZZLPB6oPu3CCDKMgCUmjdLZc9
vMNpCH6HXDlc4FW8czoOpFJXBGgF7pJ90vzkKQnKIqMKz2LrfFtiBwqFCMPtIdYX
/aj1Oa0sXXrj/ZzD+QuZdgycAf90/L0b+zWenLJaggRLqUv/PT/2SyMEldGMTRCG
c4+0qdIBhNVPjr6bIX6nOgnexFDFqvfLMy8LHq7LV20xqJDZiu3Xth1BBbCKId+a
rngLU7oGSaIGSfhqLm02F3XPniMktrVkKc00aNzwAOBLLgiWpJzuwcyKCkDhAiBZ
1rfniF6gW/PTWnZ4rscTiF4Dn8iKcr7r7LY+skEuXQKBgQDV9V8tBRnE/qUVqgjK
j0Z7kwhTFWlCaLEqNzfXuV2C2QPYdF4B4nxIUILSMpkgtbDrW7OZBwMk4ac5FCvS
4ok5LcisHs9MgBb4thyf5J+ZKTOM7Z0W+h1MgIs9Bful/PkPHyfNd8rFnhHjGIr9
05Uqewu8Jwxz8K3hZs1/3fyVlwKBgQDKwOSNCaoWqDi92/vJphoDZOE0vlB3Ti24
Rg57IlMJZh/l74Qu380E9tC6YidN18yAd4Z9xSBde6ymTdfc8SaID2aIlTZh1kUy
TlWM+JZS5Nzwywb7dBJK/u6+Foe7HsYqUb8Sog/ne6ox0fFIh4thwO1Hh9uUv63L
O80UFXaOcwKBgQDEe4jjtwNrPM4tjvBz1A9N/EBwzADV036e3gaSPM/7EX/Oj06l
PHAVmJoKnhyxRSkrehL8PMxOWktOx49XImIR+FGIfuKvxhFSZSr0Suelp4iHqs3Q
A/BUCNfVOmFWlXHCyUGsFo5H3Flgy3EYl+0sDcNBDjsJXcTQca/V9O24EQKBgBfy
0swp8RI+Cn26hzIZUYdHGia9uAlvjYzvkXRP6Jj6nBfvw6A5xSCp+puZTmUucTRX
aeZfK2R/YDRAi5fIUDHQB99oKIVD5uZ7RDWjgzYFXGeAw7Fd029ST2baiGu8xdFn
2Hbd95zzCXZbAvH7OKZyQFSrom8eeOvBg4a0xk0rAoGBAMYguMmRtSXHNy5VYr02
JnDUkEEo+qr9pb+z57OqzP1tDVQzjqovy1MQUrI9jLX78lky23P6Qi3mJATu73qo
a14TIRGxPYzfYplhzWqi+LonohKmvRG+Gm6u82abesqgIjUuZmvyxQa7grMi90h8
p5t8O+ki5NtPp+RAy5pDZg5Z
-----END PRIVATE KEY-----