Merge pull request #439 from tobychui/v3.1.5

Fixed hostname case sensitive bug
Fixed ACME table too wide css bug
Fixed HSTS toggle button bug
Fixed slow GeoIP resolve mode concurrent r/w bug
Added close connection as default site option
Added experimental authelia support
Added custom header support to websocket
Added levelDB as database implementation (not currently used)
Added external GeoIP db loading support
Restructured a lot of modules
This commit is contained in:
Toby Chui 2024-12-27 22:12:55 +08:00 committed by GitHub
commit 85422c0a74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 27830 additions and 23003 deletions

View File

@ -44,6 +44,7 @@ ENV ZEROTIER="false"
ENV AUTORENEW="86400" ENV AUTORENEW="86400"
ENV CFGUPGRADE="true" ENV CFGUPGRADE="true"
ENV DB="auto"
ENV DOCKER="true" ENV DOCKER="true"
ENV EARLYRENEW="30" ENV EARLYRENEW="30"
ENV FASTGEOIP="false" ENV FASTGEOIP="false"
@ -52,6 +53,7 @@ ENV MDNSNAME="''"
ENV NOAUTH="false" ENV NOAUTH="false"
ENV PORT="8000" ENV PORT="8000"
ENV SSHLB="false" ENV SSHLB="false"
ENV UPDATE_GEOIP="false"
ENV VERSION="false" ENV VERSION="false"
ENV WEBFM="true" ENV WEBFM="true"
ENV WEBROOT="./www" ENV WEBROOT="./www"

View File

@ -73,6 +73,7 @@ Variables are the same as those in [Start Parameters](https://github.com/tobychu
|:-|:-|:-| |:-|:-|:-|
| `AUTORENEW` | `86400` (Integer) | ACME auto TLS/SSL certificate renew check interval. | | `AUTORENEW` | `86400` (Integer) | ACME auto TLS/SSL certificate renew check interval. |
| `CFGUPGRADE` | `true` (Boolean) | Enable auto config upgrade if breaking change is detected. | | `CFGUPGRADE` | `true` (Boolean) | Enable auto config upgrade if breaking change is detected. |
| `DB` | `auto` (String) | Database backend to use (leveldb, boltdb, auto) Note that fsdb will be used on unsupported platforms like RISCV (default "auto"). |
| `DOCKER` | `true` (Boolean) | Run Zoraxy in docker compatibility mode. | | `DOCKER` | `true` (Boolean) | Run Zoraxy in docker compatibility mode. |
| `EARLYRENEW` | `30` (Integer) | Number of days to early renew a soon expiring certificate. | | `EARLYRENEW` | `30` (Integer) | Number of days to early renew a soon expiring certificate. |
| `FASTGEOIP` | `false` (Boolean) | Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices). | | `FASTGEOIP` | `false` (Boolean) | Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices). |
@ -81,6 +82,7 @@ Variables are the same as those in [Start Parameters](https://github.com/tobychu
| `NOAUTH` | `false` (Boolean) | Disable authentication for management interface. | | `NOAUTH` | `false` (Boolean) | Disable authentication for management interface. |
| `PORT` | `8000` (Integer) | Management web interface listening port | | `PORT` | `8000` (Integer) | Management web interface listening port |
| `SSHLB` | `false` (Boolean) | Allow loopback web ssh connection (DANGER). | | `SSHLB` | `false` (Boolean) | Allow loopback web ssh connection (DANGER). |
| `UPDATE_GEOIP` | `false` (Boolean) | Download the latest GeoIP data and exit. |
| `VERSION` | `false` (Boolean) | Show version of this server. | | `VERSION` | `false` (Boolean) | Show version of this server. |
| `WEBFM` | `true` (Boolean) | Enable web file manager for static web server root folder. | | `WEBFM` | `true` (Boolean) | Enable web file manager for static web server root folder. |
| `WEBROOT` | `./www` (String) | Static web server root folder. Only allow change in start parameters. | | `WEBROOT` | `./www` (String) | Static web server root folder. Only allow change in start parameters. |

View File

@ -1,7 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
update-ca-certificates update-ca-certificates
echo "CA certificates updated" echo "CA certificates updated."
zoraxy -update_geoip=true
echo "Updated GeoIP data."
if [ "$ZEROTIER" = "true" ]; then if [ "$ZEROTIER" = "true" ]; then
if [ ! -d "/opt/zoraxy/config/zerotier/" ]; then if [ ! -d "/opt/zoraxy/config/zerotier/" ]; then
@ -9,13 +12,14 @@ if [ "$ZEROTIER" = "true" ]; then
fi fi
ln -s /opt/zoraxy/config/zerotier/ /var/lib/zerotier-one ln -s /opt/zoraxy/config/zerotier/ /var/lib/zerotier-one
zerotier-one -d zerotier-one -d
echo "ZeroTier daemon started" echo "ZeroTier daemon started."
fi fi
echo "Starting Zoraxy..." echo "Starting Zoraxy..."
exec zoraxy \ exec zoraxy \
-autorenew="$AUTORENEW" \ -autorenew="$AUTORENEW" \
-cfgupgrade="$CFGUPGRADE" \ -cfgupgrade="$CFGUPGRADE" \
-db="$DB" \
-docker="$DOCKER" \ -docker="$DOCKER" \
-earlyrenew="$EARLYRENEW" \ -earlyrenew="$EARLYRENEW" \
-fastgeoip="$FASTGEOIP" \ -fastgeoip="$FASTGEOIP" \
@ -24,6 +28,7 @@ exec zoraxy \
-noauth="$NOAUTH" \ -noauth="$NOAUTH" \
-port=:"$PORT" \ -port=:"$PORT" \
-sshlb="$SSHLB" \ -sshlb="$SSHLB" \
-update_geoip="$UPDATE_GEOIP" \
-version="$VERSION" \ -version="$VERSION" \
-webfm="$WEBFM" \ -webfm="$WEBFM" \
-webroot="$WEBROOT" \ -webroot="$WEBROOT" \

View File

@ -77,21 +77,9 @@ func RegisterTLSAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/cert/delete", handleCertRemove) authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
} }
// Register the APIs for SSO and Oauth functions, WIP // Register the APIs for Authentication handlers like Authelia and OAUTH2
func RegisterSSOAPIs(authRouter *auth.RouterDef) { func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/sso/status", ssoHandler.HandleSSOStatus) authRouter.HandleFunc("/api/sso/Authelia", autheliaRouter.HandleSetAutheliaURLAndHTTPS)
authRouter.HandleFunc("/api/sso/enable", ssoHandler.HandleSSOEnable)
authRouter.HandleFunc("/api/sso/setPort", ssoHandler.HandlePortChange)
authRouter.HandleFunc("/api/sso/setAuthURL", ssoHandler.HandleSetAuthURL)
authRouter.HandleFunc("/api/sso/app/register", ssoHandler.HandleRegisterApp)
//authRouter.HandleFunc("/api/sso/app/list", ssoHandler.HandleListApp)
//authRouter.HandleFunc("/api/sso/app/remove", ssoHandler.HandleRemoveApp)
authRouter.HandleFunc("/api/sso/user/list", ssoHandler.HandleListUser)
authRouter.HandleFunc("/api/sso/user/add", ssoHandler.HandleAddUser)
authRouter.HandleFunc("/api/sso/user/edit", ssoHandler.HandleEditUser)
authRouter.HandleFunc("/api/sso/user/remove", ssoHandler.HandleRemoveUser)
} }
// Register the APIs for redirection rules management functions // Register the APIs for redirection rules management functions
@ -339,7 +327,7 @@ func initAPIs(targetMux *http.ServeMux) {
RegisterAuthAPIs(requireAuth, targetMux) RegisterAuthAPIs(requireAuth, targetMux)
RegisterHTTPProxyAPIs(authRouter) RegisterHTTPProxyAPIs(authRouter)
RegisterTLSAPIs(authRouter) RegisterTLSAPIs(authRouter)
//RegisterSSOAPIs(authRouter) RegisterAuthenticationHandlerAPIs(authRouter)
RegisterRedirectionAPIs(authRouter) RegisterRedirectionAPIs(authRouter)
RegisterAccessRuleAPIs(authRouter) RegisterAccessRuleAPIs(authRouter)
RegisterPathRuleAPIs(authRouter) RegisterPathRuleAPIs(authRouter)

View File

@ -59,7 +59,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
thisConfigEndpoint.RootOrMatchingDomain = "/" thisConfigEndpoint.RootOrMatchingDomain = "/"
} }
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Root { if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
//This is a root config file //This is a root config file
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint) rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil { if err != nil {
@ -68,7 +68,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint) dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint)
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Host { } else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeHost {
//This is a host config file //This is a host config file
readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint) readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil { if err != nil {
@ -97,7 +97,7 @@ func filterProxyConfigFilename(filename string) string {
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error { func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
//Get filename for saving //Get filename for saving
filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config") filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config")
if endpoint.ProxyType == dynamicproxy.ProxyType_Root { if endpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
filename = "./conf/proxy/root.config" filename = "./conf/proxy/root.config"
} }
@ -129,9 +129,15 @@ func RemoveReverseProxyConfig(endpoint string) error {
// Get the default root config that point to the internal static web server // Get the default root config that point to the internal static web server
// this will be used if root config is not found (new deployment / missing root.config file) // this will be used if root config is not found (new deployment / missing root.config file)
func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) { func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
//Default Authentication Provider
defaultAuth := &dynamicproxy.AuthenticationProvider{
AuthMethod: dynamicproxy.AuthMethodNone,
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
}
//Default settings //Default settings
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&dynamicproxy.ProxyEndpoint{ rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&dynamicproxy.ProxyEndpoint{
ProxyType: dynamicproxy.ProxyType_Root, ProxyType: dynamicproxy.ProxyTypeRoot,
RootOrMatchingDomain: "/", RootOrMatchingDomain: "/",
ActiveOrigins: []*loadbalance.Upstream{ ActiveOrigins: []*loadbalance.Upstream{
{ {
@ -144,9 +150,7 @@ func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
InactiveOrigins: []*loadbalance.Upstream{}, InactiveOrigins: []*loadbalance.Upstream{},
BypassGlobalTLS: false, BypassGlobalTLS: false,
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{}, VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
RequireBasicAuth: false, AuthenticationProvider: defaultAuth,
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
DefaultSiteOption: dynamicproxy.DefaultSite_InternalStaticWebServer, DefaultSiteOption: dynamicproxy.DefaultSite_InternalStaticWebServer,
DefaultSiteValue: "", DefaultSiteValue: "",
}) })
@ -167,7 +171,6 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
if includeSysDBRaw == "true" { if includeSysDBRaw == "true" {
//Include the system database in backup snapshot //Include the system database in backup snapshot
//Temporary set it to read only //Temporary set it to read only
sysdb.ReadOnly = true
includeSysDB = true includeSysDB = true
} }
@ -241,8 +244,6 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
return return
} }
//Restore sysdb state
sysdb.ReadOnly = false
} }
if err != nil { if err != nil {

View File

@ -16,7 +16,7 @@ import (
"imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth" "imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/auth/sso" "imuslab.com/zoraxy/mod/auth/sso/authelia"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dockerux" "imuslab.com/zoraxy/mod/dockerux"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
@ -42,7 +42,7 @@ import (
const ( const (
/* Build Constants */ /* Build Constants */
SYSTEM_NAME = "Zoraxy" SYSTEM_NAME = "Zoraxy"
SYSTEM_VERSION = "3.1.4" SYSTEM_VERSION = "3.1.5"
DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */ DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */
/* System Constants */ /* System Constants */
@ -74,6 +74,7 @@ const (
/* System Startup Flags */ /* System Startup Flags */
var ( var (
webUIPort = flag.String("port", ":8000", "Management web interface listening port") webUIPort = flag.String("port", ":8000", "Management web interface listening port")
databaseBackend = flag.String("db", "auto", "Database backend to use (leveldb, boltdb, auto) Note that fsdb will be used on unsupported platforms like RISCV")
noauth = flag.Bool("noauth", false, "Disable authentication for management interface") noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
showver = flag.Bool("version", false, "Show version of this server") showver = flag.Bool("version", false, "Show version of this server")
allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)") allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
@ -88,6 +89,9 @@ var (
staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters") staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters")
allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder") allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected") enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected")
/* Maintaince Function Flags */
geoDbUpdate = flag.Bool("update_geoip", false, "Download the latest GeoIP data and exit")
) )
/* Global Variables and Handlers */ /* Global Variables and Handlers */
@ -127,7 +131,9 @@ var (
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing
ssoHandler *sso.SSOHandler //Single Sign On handler
//Authentication Provider
autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
//Helper modules //Helper modules
EmailSender *email.Sender //Email sender that handle email sending EmailSender *email.Sender //Email sender that handle email sending

View File

@ -28,9 +28,11 @@ require (
github.com/benbjohnson/clock v1.3.0 // indirect github.com/benbjohnson/clock v1.3.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 // indirect github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/shopspring/decimal v1.3.1 // indirect github.com/shopspring/decimal v1.3.1 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
github.com/tidwall/buntdb v1.1.2 // indirect github.com/tidwall/buntdb v1.1.2 // indirect
github.com/tidwall/gjson v1.12.1 // indirect github.com/tidwall/gjson v1.12.1 // indirect

View File

@ -277,6 +277,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@ -528,6 +530,7 @@ github.com/nzdjb/go-metaname v1.0.0 h1:sNASlZC1RM3nSudtBTE1a3ZVTDyTpjqI5WXRPrdZ9
github.com/nzdjb/go-metaname v1.0.0/go.mod h1:0GR0LshZax1Lz4VrOrfNSE4dGvTp7HGjiemdczXT2H4= github.com/nzdjb/go-metaname v1.0.0/go.mod h1:0GR0LshZax1Lz4VrOrfNSE4dGvTp7HGjiemdczXT2H4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
@ -536,6 +539,7 @@ github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=
github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
@ -660,6 +664,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 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/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 h1:RE84sHFFx6t24DJvSnF9fS1DzBNv9OpctzHK3t7AY+I= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 h1:RE84sHFFx6t24DJvSnF9fS1DzBNv9OpctzHK3t7AY+I=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 h1:QwE0dRkAAbdf+eACnkNULgDn9ZKUJpPWRyXdqJolP5E= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 h1:QwE0dRkAAbdf+eACnkNULgDn9ZKUJpPWRyXdqJolP5E=

View File

@ -42,11 +42,11 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/update" "imuslab.com/zoraxy/mod/update"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
/* SIGTERM handler, do shutdown sequences before closing */ /* SIGTERM handler, do shutdown sequences before closing */
func SetupCloseHandler() { func SetupCloseHandler() {
c := make(chan os.Signal, 2) c := make(chan os.Signal, 2)
@ -58,43 +58,21 @@ func SetupCloseHandler() {
}() }()
} }
func ShutdownSeq() {
SystemWideLogger.Println("Shutting down " + SYSTEM_NAME)
SystemWideLogger.Println("Closing Netstats Listener")
netstatBuffers.Close()
SystemWideLogger.Println("Closing Statistic Collector")
statisticCollector.Close()
if mdnsTickerStop != nil {
SystemWideLogger.Println("Stopping mDNS Discoverer (might take a few minutes)")
// Stop the mdns service
mdnsTickerStop <- true
}
mdnsScanner.Close()
SystemWideLogger.Println("Shutting down load balancer")
loadBalancer.Close()
SystemWideLogger.Println("Closing Certificates Auto Renewer")
acmeAutoRenewer.Close()
//Remove the tmp folder
SystemWideLogger.Println("Cleaning up tmp files")
os.RemoveAll("./tmp")
//Close database
SystemWideLogger.Println("Stopping system database")
sysdb.Close()
//Close logger
SystemWideLogger.Println("Closing system wide logger")
SystemWideLogger.Close()
}
func main() { func main() {
//Parse startup flags //Parse startup flags
flag.Parse() flag.Parse()
/* Maintaince Function Modes */
if *showver { if *showver {
fmt.Println(SYSTEM_NAME + " - Version " + SYSTEM_VERSION) fmt.Println(SYSTEM_NAME + " - Version " + SYSTEM_VERSION)
os.Exit(0) os.Exit(0)
} }
if *geoDbUpdate {
geodb.DownloadGeoDBUpdate("./conf/geodb")
os.Exit(0)
}
/* Main Zoraxy Routines */
if !utils.ValidateListeningAddress(*webUIPort) { if !utils.ValidateListeningAddress(*webUIPort) {
fmt.Println("Malformed -port (listening address) paramter. Do you mean -port=:" + *webUIPort + "?") fmt.Println("Malformed -port (listening address) paramter. Do you mean -port=:" + *webUIPort + "?")
os.Exit(0) os.Exit(0)
@ -130,7 +108,7 @@ func main() {
csrf.SameSite(csrf.SameSiteLaxMode), csrf.SameSite(csrf.SameSiteLaxMode),
) )
//Startup all modules //Startup all modules, see start.go
startupSequence() startupSequence()
//Initiate management interface APIs //Initiate management interface APIs

View File

@ -1,34 +0,0 @@
package sso
/*
app.go
This file contains the app structure and app management
functions for the SSO module.
*/
// RegisteredUpstreamApp is a structure that contains the information of an
// upstream app that is registered with the SSO server
type RegisteredUpstreamApp struct {
ID string
Secret string
Domain []string
Scopes []string
SessionDuration int //in seconds, default to 1 hour
}
// RegisterUpstreamApp registers an upstream app with the SSO server
func (s *SSOHandler) ListRegisteredApps() []*RegisteredUpstreamApp {
apps := make([]*RegisteredUpstreamApp, 0)
for _, app := range s.Apps {
apps = append(apps, &app)
}
return apps
}
// RegisterUpstreamApp registers an upstream app with the SSO server
func (s *SSOHandler) GetAppByID(appID string) (*RegisteredUpstreamApp, bool) {
app, ok := s.Apps[appID]
return &app, ok
}

View File

@ -0,0 +1,136 @@
package authelia
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
type AutheliaRouterOptions struct {
UseHTTPS bool //If the Authelia server is using HTTPS
AutheliaURL string //The URL of the Authelia server
Logger *logger.Logger
Database *database.Database
}
type AutheliaRouter struct {
options *AutheliaRouterOptions
}
// NewAutheliaRouter creates a new AutheliaRouter object
func NewAutheliaRouter(options *AutheliaRouterOptions) *AutheliaRouter {
options.Database.NewTable("authelia")
//Read settings from database, if exists
options.Database.Read("authelia", "autheliaURL", &options.AutheliaURL)
options.Database.Read("authelia", "useHTTPS", &options.UseHTTPS)
return &AutheliaRouter{
options: options,
}
}
// HandleSetAutheliaURLAndHTTPS is the internal handler for setting the Authelia URL and HTTPS
func (ar *AutheliaRouter) HandleSetAutheliaURLAndHTTPS(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current settings
js, _ := json.Marshal(map[string]interface{}{
"useHTTPS": ar.options.UseHTTPS,
"autheliaURL": ar.options.AutheliaURL,
})
utils.SendJSONResponse(w, string(js))
return
} else if r.Method == http.MethodPost {
//Update the settings
autheliaURL, err := utils.PostPara(r, "autheliaURL")
if err != nil {
utils.SendErrorResponse(w, "autheliaURL not found")
return
}
useHTTPS, err := utils.PostBool(r, "useHTTPS")
if err != nil {
useHTTPS = false
}
//Write changes to runtime
ar.options.AutheliaURL = autheliaURL
ar.options.UseHTTPS = useHTTPS
//Write changes to database
ar.options.Database.Write("authelia", "autheliaURL", autheliaURL)
ar.options.Database.Write("authelia", "useHTTPS", useHTTPS)
utils.SendOK(w)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
}
// handleAutheliaAuth is the internal handler for Authelia authentication
// Set useHTTPS to true if your authelia server is using HTTPS
// Set autheliaURL to the URL of the Authelia server, e.g. authelia.example.com
func (ar *AutheliaRouter) HandleAutheliaAuth(w http.ResponseWriter, r *http.Request) error {
client := &http.Client{}
if ar.options.AutheliaURL == "" {
ar.options.Logger.PrintAndLog("Authelia", "Authelia URL not set", nil)
w.WriteHeader(500)
w.Write([]byte("500 - Internal Server Error"))
return errors.New("authelia URL not set")
}
protocol := "http"
if ar.options.UseHTTPS {
protocol = "https"
}
autheliaBaseURL := protocol + "://" + ar.options.AutheliaURL
//Remove tailing slash if any
if autheliaBaseURL[len(autheliaBaseURL)-1] == '/' {
autheliaBaseURL = autheliaBaseURL[:len(autheliaBaseURL)-1]
}
//Make a request to Authelia to verify the request
req, err := http.NewRequest("POST", autheliaBaseURL+"/api/verify", nil)
if err != nil {
ar.options.Logger.PrintAndLog("Authelia", "Unable to create request", err)
w.WriteHeader(401)
return errors.New("unauthorized")
}
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
req.Header.Add("X-Original-URL", fmt.Sprintf("%s://%s", scheme, r.Host))
// Copy cookies from the incoming request
for _, cookie := range r.Cookies() {
req.AddCookie(cookie)
}
// Making the verification request
resp, err := client.Do(req)
if err != nil {
ar.options.Logger.PrintAndLog("Authelia", "Unable to verify", err)
w.WriteHeader(401)
return errors.New("unauthorized")
}
if resp.StatusCode != 200 {
redirectURL := autheliaBaseURL + "/?rd=" + url.QueryEscape(scheme+"://"+r.Host+r.URL.String()) + "&rm=" + r.Method
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
return errors.New("unauthorized")
}
return nil
}

View File

@ -1,271 +0,0 @@
package sso
/*
handlers.go
This file contains the handlers for the SSO module.
If you are looking for handlers for SSO user management,
please refer to userHandlers.go.
*/
import (
"encoding/json"
"net/http"
"strings"
"github.com/gofrs/uuid"
"imuslab.com/zoraxy/mod/utils"
)
// HandleSSOStatus handle the request to get the status of the SSO portal server
func (s *SSOHandler) HandleSSOStatus(w http.ResponseWriter, r *http.Request) {
type SSOStatus struct {
Enabled bool
SSOInterceptEnabled bool
ListeningPort int
AuthURL string
}
status := SSOStatus{
Enabled: s.ssoPortalServer != nil,
//SSOInterceptEnabled: s.ssoInterceptEnabled,
ListeningPort: s.Config.PortalServerPort,
AuthURL: s.Config.AuthURL,
}
js, _ := json.Marshal(status)
utils.SendJSONResponse(w, string(js))
}
// Wrapper for starting and stopping the SSO portal server
// require POST request with key "enable" and value "true" or "false"
func (s *SSOHandler) HandleSSOEnable(w http.ResponseWriter, r *http.Request) {
enable, err := utils.PostBool(r, "enable")
if err != nil {
utils.SendErrorResponse(w, "invalid enable value")
return
}
if enable {
s.HandleStartSSOPortal(w, r)
} else {
s.HandleStopSSOPortal(w, r)
}
}
// HandleStartSSOPortal handle the request to start the SSO portal server
func (s *SSOHandler) HandleStartSSOPortal(w http.ResponseWriter, r *http.Request) {
if s.ssoPortalServer != nil {
//Already enabled. Do restart instead.
err := s.RestartSSOServer()
if err != nil {
utils.SendErrorResponse(w, "failed to start SSO server")
return
}
utils.SendOK(w)
return
}
//Check if the authURL is set correctly. If not, return error
if s.Config.AuthURL == "" {
utils.SendErrorResponse(w, "auth URL not set")
return
}
//Start the SSO portal server in go routine
go s.StartSSOPortal()
//Write current state to database
err := s.Config.Database.Write("sso_conf", "enabled", true)
if err != nil {
utils.SendErrorResponse(w, "failed to update SSO state")
return
}
utils.SendOK(w)
}
// HandleStopSSOPortal handle the request to stop the SSO portal server
func (s *SSOHandler) HandleStopSSOPortal(w http.ResponseWriter, r *http.Request) {
if s.ssoPortalServer == nil {
//Already disabled
utils.SendOK(w)
return
}
err := s.ssoPortalServer.Close()
if err != nil {
s.Log("Failed to stop SSO portal server", err)
utils.SendErrorResponse(w, "failed to stop SSO portal server")
return
}
s.ssoPortalServer = nil
//Write current state to database
err = s.Config.Database.Write("sso_conf", "enabled", false)
if err != nil {
utils.SendErrorResponse(w, "failed to update SSO state")
return
}
utils.SendOK(w)
}
// HandlePortChange handle the request to change the SSO portal server port
func (s *SSOHandler) HandlePortChange(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current port
js, _ := json.Marshal(s.Config.PortalServerPort)
utils.SendJSONResponse(w, string(js))
return
}
port, err := utils.PostInt(r, "port")
if err != nil {
utils.SendErrorResponse(w, "invalid port given")
return
}
s.Config.PortalServerPort = port
//Write to the database
err = s.Config.Database.Write("sso_conf", "port", port)
if err != nil {
utils.SendErrorResponse(w, "failed to update port")
return
}
if s.IsRunning() {
//Restart the server if it is running
err = s.RestartSSOServer()
if err != nil {
utils.SendErrorResponse(w, "failed to restart SSO server")
return
}
}
utils.SendOK(w)
}
// HandleSetAuthURL handle the request to change the SSO auth URL
// This is the URL that the SSO portal server will redirect to for authentication
// e.g. auth.yourdomain.com
func (s *SSOHandler) HandleSetAuthURL(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current auth URL
js, _ := json.Marshal(s.Config.AuthURL)
utils.SendJSONResponse(w, string(js))
return
}
//Get the auth URL
authURL, err := utils.PostPara(r, "auth_url")
if err != nil {
utils.SendErrorResponse(w, "invalid auth URL given")
return
}
s.Config.AuthURL = authURL
//Write to the database
err = s.Config.Database.Write("sso_conf", "authurl", authURL)
if err != nil {
utils.SendErrorResponse(w, "failed to update auth URL")
return
}
//Clear the cookie store and restart the server
err = s.RestartSSOServer()
if err != nil {
utils.SendErrorResponse(w, "failed to restart SSO server")
return
}
utils.SendOK(w)
}
// HandleRegisterApp handle the request to register a new app to the SSO portal
func (s *SSOHandler) HandleRegisterApp(w http.ResponseWriter, r *http.Request) {
appName, err := utils.PostPara(r, "app_name")
if err != nil {
utils.SendErrorResponse(w, "invalid app name given")
return
}
id, err := utils.PostPara(r, "app_id")
if err != nil {
//If id is not given, use the app name with a random UUID
newID, err := uuid.NewV4()
if err != nil {
utils.SendErrorResponse(w, "failed to generate new app ID")
return
}
id = strings.ReplaceAll(appName, " ", "") + "-" + newID.String()
}
//Check if the given appid is already in use
if _, ok := s.Apps[id]; ok {
utils.SendErrorResponse(w, "app ID already in use")
return
}
/*
Process the app domain
An app can have multiple domains, separated by commas
Usually the app domain is the proxy rule that points to the app
For example, if the app is hosted at app.yourdomain.com, the app domain is app.yourdomain.com
*/
appDomain, err := utils.PostPara(r, "app_domain")
if err != nil {
utils.SendErrorResponse(w, "invalid app URL given")
return
}
appURLs := strings.Split(appDomain, ",")
//Remove padding and trailing spaces in each URL
for i := range appURLs {
appURLs[i] = strings.TrimSpace(appURLs[i])
}
//Create a new app entry
thisAppEntry := RegisteredUpstreamApp{
ID: id,
Secret: "",
Domain: appURLs,
Scopes: []string{},
SessionDuration: 3600,
}
js, _ := json.Marshal(thisAppEntry)
//Create a new app in the database
err = s.Config.Database.Write("sso_apps", appName, string(js))
if err != nil {
utils.SendErrorResponse(w, "failed to create new app")
return
}
//Also add the app to runtime config
s.Apps[appName] = thisAppEntry
utils.SendOK(w)
}
// HandleAppRemove handle the request to remove an app from the SSO portal
func (s *SSOHandler) HandleAppRemove(w http.ResponseWriter, r *http.Request) {
appID, err := utils.PostPara(r, "app_id")
if err != nil {
utils.SendErrorResponse(w, "invalid app ID given")
return
}
//Check if the app actually exists
if _, ok := s.Apps[appID]; !ok {
utils.SendErrorResponse(w, "app not found")
return
}
delete(s.Apps, appID)
//Also remove it from the database
err = s.Config.Database.Delete("sso_apps", appID)
if err != nil {
s.Log("Failed to remove app from database", err)
}
}

View File

@ -1,295 +0,0 @@
package sso
import (
"context"
_ "embed"
"encoding/json"
"log"
"net/http"
"net/url"
"time"
"github.com/go-oauth2/oauth2/v4/errors"
"github.com/go-oauth2/oauth2/v4/generates"
"github.com/go-oauth2/oauth2/v4/manage"
"github.com/go-oauth2/oauth2/v4/models"
"github.com/go-oauth2/oauth2/v4/server"
"github.com/go-oauth2/oauth2/v4/store"
"github.com/go-session/session"
"imuslab.com/zoraxy/mod/utils"
)
const (
SSO_SESSION_NAME = "ZoraxySSO"
)
type OAuth2Server struct {
srv *server.Server //oAuth server instance
config *SSOConfig
parent *SSOHandler
}
//go:embed static/auth.html
var authHtml []byte
//go:embed static/login.html
var loginHtml []byte
// NewOAuth2Server creates a new OAuth2 server instance
func NewOAuth2Server(config *SSOConfig, parent *SSOHandler) (*OAuth2Server, error) {
manager := manage.NewDefaultManager()
manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
// token store
manager.MustTokenStorage(store.NewFileTokenStore("./conf/sso.db"))
// generate jwt access token
manager.MapAccessGenerate(generates.NewAccessGenerate())
//Load the information of registered app within the OAuth2 server
clientStore := store.NewClientStore()
clientStore.Set("myapp", &models.Client{
ID: "myapp",
Secret: "verysecurepassword",
Domain: "localhost:9094",
})
//TODO: LOAD THIS DYNAMICALLY FROM DATABASE
manager.MapClientStorage(clientStore)
thisServer := OAuth2Server{
config: config,
parent: parent,
}
//Create a new oauth server
srv := server.NewServer(server.NewConfig(), manager)
srv.SetPasswordAuthorizationHandler(thisServer.PasswordAuthorizationHandler)
srv.SetUserAuthorizationHandler(thisServer.UserAuthorizeHandler)
srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
log.Println("Internal Error:", err.Error())
return
})
srv.SetResponseErrorHandler(func(re *errors.Response) {
log.Println("Response Error:", re.Error.Error())
})
//Set the access scope handler
srv.SetAuthorizeScopeHandler(thisServer.AuthorizationScopeHandler)
//Set the access token expiration handler based on requesting domain / hostname
srv.SetAccessTokenExpHandler(thisServer.ExpireHandler)
thisServer.srv = srv
return &thisServer, nil
}
// Password handler, validate if the given username and password are correct
func (oas *OAuth2Server) PasswordAuthorizationHandler(ctx context.Context, clientID, username, password string) (userID string, err error) {
//TODO: LOAD THIS DYNAMICALLY FROM DATABASE
if username == "test" && password == "test" {
userID = "test"
}
return
}
// User Authorization Handler, handle auth request from user
func (oas *OAuth2Server) UserAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string, err error) {
store, err := session.Start(r.Context(), w, r)
if err != nil {
return
}
uid, ok := store.Get(SSO_SESSION_NAME)
if !ok {
if r.Form == nil {
r.ParseForm()
}
store.Set("ReturnUri", r.Form)
store.Save()
w.Header().Set("Location", "/oauth2/login")
w.WriteHeader(http.StatusFound)
return
}
userID = uid.(string)
store.Delete(SSO_SESSION_NAME)
store.Save()
return
}
// AccessTokenExpHandler, set the SSO session length default value
func (oas *OAuth2Server) ExpireHandler(w http.ResponseWriter, r *http.Request) (exp time.Duration, err error) {
requestHostname := r.Host
if requestHostname == "" {
//Use default value
return time.Hour, nil
}
//Get the Registered App Config from parent
appConfig, ok := oas.parent.Apps[requestHostname]
if !ok {
//Use default value
return time.Hour, nil
}
//Use the app's session length
return time.Second * time.Duration(appConfig.SessionDuration), nil
}
// AuthorizationScopeHandler, handle the scope of the request
func (oas *OAuth2Server) AuthorizationScopeHandler(w http.ResponseWriter, r *http.Request) (scope string, err error) {
//Get the scope from post or GEt request
if r.Form == nil {
if err := r.ParseForm(); err != nil {
return "none", err
}
}
//Get the hostname of the request
requestHostname := r.Host
if requestHostname == "" {
//No rule set. Use default
return "none", nil
}
//Get the Registered App Config from parent
appConfig, ok := oas.parent.Apps[requestHostname]
if !ok {
//No rule set. Use default
return "none", nil
}
//Check if the scope is set in the request
if v, ok := r.Form["scope"]; ok {
//Check if the requested scope is in the appConfig scope
if utils.StringInArray(appConfig.Scopes, v[0]) {
return v[0], nil
}
return "none", nil
}
return "none", nil
}
/* SSO Web Server Toggle Functions */
func (oas *OAuth2Server) RegisterOauthEndpoints(primaryMux *http.ServeMux) {
primaryMux.HandleFunc("/oauth2/login", oas.loginHandler)
primaryMux.HandleFunc("/oauth2/auth", oas.authHandler)
primaryMux.HandleFunc("/oauth2/authorize", func(w http.ResponseWriter, r *http.Request) {
store, err := session.Start(r.Context(), w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var form url.Values
if v, ok := store.Get("ReturnUri"); ok {
form = v.(url.Values)
}
r.Form = form
store.Delete("ReturnUri")
store.Save()
err = oas.srv.HandleAuthorizeRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
})
primaryMux.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) {
err := oas.srv.HandleTokenRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
primaryMux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
token, err := oas.srv.ValidationBearerToken(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
data := map[string]interface{}{
"expires_in": int64(time.Until(token.GetAccessCreateAt().Add(token.GetAccessExpiresIn())).Seconds()),
"client_id": token.GetClientID(),
"user_id": token.GetUserID(),
}
e := json.NewEncoder(w)
e.SetIndent("", " ")
e.Encode(data)
})
}
func (oas *OAuth2Server) loginHandler(w http.ResponseWriter, r *http.Request) {
store, err := session.Start(r.Context(), w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if r.Method == "POST" {
if r.Form == nil {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
//Load username and password from form post
username, err := utils.PostPara(r, "username")
if err != nil {
w.Write([]byte("invalid username or password"))
return
}
password, err := utils.PostPara(r, "password")
if err != nil {
w.Write([]byte("invalid username or password"))
return
}
//Validate the user
if !oas.parent.ValidateUsernameAndPassword(username, password) {
//Wrong password
w.Write([]byte("invalid username or password"))
return
}
store.Set(SSO_SESSION_NAME, r.Form.Get("username"))
store.Save()
w.Header().Set("Location", "/oauth2/auth")
w.WriteHeader(http.StatusFound)
return
} else if r.Method == "GET" {
//Check if the user is logged in
if _, ok := store.Get(SSO_SESSION_NAME); ok {
w.Header().Set("Location", "/oauth2/auth")
w.WriteHeader(http.StatusFound)
return
}
}
//User not logged in. Show login page
w.Write(loginHtml)
}
func (oas *OAuth2Server) authHandler(w http.ResponseWriter, r *http.Request) {
store, err := session.Start(context.TODO(), w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, ok := store.Get(SSO_SESSION_NAME); !ok {
w.Header().Set("Location", "/oauth2/login")
w.WriteHeader(http.StatusFound)
return
}
//User logged in. Check if this user have previously authorized the app
//TODO: Check if the user have previously authorized the app
//User have not authorized the app. Show the authorization page
w.Write(authHtml)
}

View File

@ -1 +0,0 @@
package sso

View File

@ -1,58 +0,0 @@
package sso
import (
"encoding/json"
"net/http"
"strings"
)
type OpenIDConfiguration struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
JwksUri string `json:"jwks_uri"`
ResponseTypesSupported []string `json:"response_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
ClaimsSupported []string `json:"claims_supported"`
}
func (h *SSOHandler) HandleDiscoveryRequest(w http.ResponseWriter, r *http.Request) {
//Prepend https:// if not present
authBaseURL := h.Config.AuthURL
if !strings.HasPrefix(authBaseURL, "http://") && !strings.HasPrefix(authBaseURL, "https://") {
authBaseURL = "https://" + authBaseURL
}
//Handle the discovery request
discovery := OpenIDConfiguration{
Issuer: authBaseURL,
AuthorizationEndpoint: authBaseURL + "/oauth2/authorize",
TokenEndpoint: authBaseURL + "/oauth2/token",
JwksUri: authBaseURL + "/jwks.json",
ResponseTypesSupported: []string{"code", "token"},
SubjectTypesSupported: []string{"public"},
IDTokenSigningAlgValuesSupported: []string{
"RS256",
},
ClaimsSupported: []string{
"sub", //Subject, usually the user ID
"iss", //Issuer, usually the server URL
"aud", //Audience, usually the client ID
"exp", //Expiration Time
"iat", //Issued At
"email", //Email
"locale", //Locale
"name", //Full Name
"nickname", //Nickname
"preferred_username", //Preferred Username
"website", //Website
},
}
//Write the response
js, _ := json.Marshal(discovery)
w.Header().Set("Content-Type", "application/json")
w.Write(js)
}

View File

@ -1,132 +0,0 @@
package sso
import (
"context"
"net/http"
"strconv"
"time"
"github.com/go-oauth2/oauth2/v4/errors"
"imuslab.com/zoraxy/mod/utils"
)
/*
server.go
This is the web server for the SSO portal. It contains the
HTTP server and the handlers for the SSO portal.
If you are looking for handlers that changes the settings
of the SSO portale or user management, please refer to
handlers.go.
*/
func (h *SSOHandler) InitSSOPortal(portalServerPort int) {
//Create a new web server for the SSO portal
pmux := http.NewServeMux()
fs := http.FileServer(http.FS(staticFiles))
pmux.Handle("/", fs)
//Register API endpoint for the SSO portal
pmux.HandleFunc("/sso/login", h.HandleLogin)
//Register API endpoint for autodiscovery
pmux.HandleFunc("/.well-known/openid-configuration", h.HandleDiscoveryRequest)
//Register OAuth2 endpoints
h.Oauth2Server.RegisterOauthEndpoints(pmux)
h.ssoPortalMux = pmux
}
// StartSSOPortal start the SSO portal server
// This function will block the main thread, call it in a goroutine
func (h *SSOHandler) StartSSOPortal() error {
if h.ssoPortalServer != nil {
return errors.New("SSO portal server already running")
}
h.ssoPortalServer = &http.Server{
Addr: ":" + strconv.Itoa(h.Config.PortalServerPort),
Handler: h.ssoPortalMux,
}
err := h.ssoPortalServer.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
h.Log("Failed to start SSO portal server", err)
}
return err
}
// StopSSOPortal stop the SSO portal server
func (h *SSOHandler) StopSSOPortal() error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := h.ssoPortalServer.Shutdown(ctx)
if err != nil {
h.Log("Failed to stop SSO portal server", err)
return err
}
h.ssoPortalServer = nil
return nil
}
// StartSSOPortal start the SSO portal server
func (h *SSOHandler) RestartSSOServer() error {
if h.ssoPortalServer != nil {
err := h.StopSSOPortal()
if err != nil {
return err
}
}
go h.StartSSOPortal()
return nil
}
func (h *SSOHandler) IsRunning() bool {
return h.ssoPortalServer != nil
}
// HandleLogin handle the login request
func (h *SSOHandler) HandleLogin(w http.ResponseWriter, r *http.Request) {
//Handle the login request
username, err := utils.PostPara(r, "username")
if err != nil {
utils.SendErrorResponse(w, "invalid username or password")
return
}
password, err := utils.PostPara(r, "password")
if err != nil {
utils.SendErrorResponse(w, "invalid username or password")
return
}
rememberMe, err := utils.PostBool(r, "remember_me")
if err != nil {
rememberMe = false
}
//Check if the user exists
userEntry, err := h.GetSSOUser(username)
if err != nil {
utils.SendErrorResponse(w, "user not found")
return
}
//Check if the password is correct
if !userEntry.VerifyPassword(password) {
utils.SendErrorResponse(w, "incorrect password")
return
}
//Create a new session for the user
session, _ := h.cookieStore.Get(r, "Zoraxy-SSO")
session.Values["username"] = username
if rememberMe {
session.Options.MaxAge = 86400 * 15 //15 days
} else {
session.Options.MaxAge = 3600 //1 hour
}
session.Save(r, w) //Save the session
utils.SendOK(w)
}

View File

@ -1,158 +0,0 @@
package sso
import (
"embed"
"net/http"
"github.com/gorilla/sessions"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
)
/*
sso.go
This file contains the main SSO handler and the SSO configuration
structure. It also contains the main SSO handler functions.
SSO web interface are stored in the static folder, which is embedded
into the binary.
*/
//go:embed static/*
var staticFiles embed.FS //Static files for the SSO portal
type SSOConfig struct {
SystemUUID string //System UUID, should be passed in from main scope
AuthURL string //Authentication subdomain URL, e.g. auth.example.com
PortalServerPort int //SSO portal server port
Database *database.Database //System master key-value database
Logger *logger.Logger
}
// SSOHandler is the main SSO handler structure
type SSOHandler struct {
cookieStore *sessions.CookieStore
ssoPortalServer *http.Server
ssoPortalMux *http.ServeMux
Oauth2Server *OAuth2Server
Config *SSOConfig
Apps map[string]RegisteredUpstreamApp
}
// Create a new Zoraxy SSO handler
func NewSSOHandler(config *SSOConfig) (*SSOHandler, error) {
//Create a cookie store for the SSO handler
cookieStore := sessions.NewCookieStore([]byte(config.SystemUUID))
cookieStore.Options = &sessions.Options{
Path: "",
Domain: "",
MaxAge: 0,
Secure: false,
HttpOnly: false,
SameSite: 0,
}
config.Database.NewTable("sso_users") //For storing user information
config.Database.NewTable("sso_conf") //For storing SSO configuration
config.Database.NewTable("sso_apps") //For storing registered apps
//Create the SSO Handler
thisHandler := SSOHandler{
cookieStore: cookieStore,
Config: config,
}
//Read the app info from database
thisHandler.Apps = make(map[string]RegisteredUpstreamApp)
//Create an oauth2 server
oauth2Server, err := NewOAuth2Server(config, &thisHandler)
if err != nil {
return nil, err
}
//Register endpoints
thisHandler.Oauth2Server = oauth2Server
thisHandler.InitSSOPortal(config.PortalServerPort)
return &thisHandler, nil
}
func (h *SSOHandler) RestorePreviousRunningState() {
//Load the previous SSO state
ssoEnabled := false
ssoPort := 5488
ssoAuthURL := ""
h.Config.Database.Read("sso_conf", "enabled", &ssoEnabled)
h.Config.Database.Read("sso_conf", "port", &ssoPort)
h.Config.Database.Read("sso_conf", "authurl", &ssoAuthURL)
if ssoAuthURL == "" {
//Cannot enable SSO without auth URL
ssoEnabled = false
}
h.Config.PortalServerPort = ssoPort
h.Config.AuthURL = ssoAuthURL
if ssoEnabled {
go h.StartSSOPortal()
}
}
// ServeForwardAuth handle the SSO request in interception mode
// Suppose to be called in dynamicproxy.
// Return true if the request is allowed to pass, false if the request is blocked and shall not be further processed
func (h *SSOHandler) ServeForwardAuth(w http.ResponseWriter, r *http.Request) bool {
//Get the current uri for appending to the auth subdomain
originalRequestURL := r.RequestURI
redirectAuthURL := h.Config.AuthURL
if redirectAuthURL == "" || !h.IsRunning() {
//Redirect not set or auth server is offlined
w.Write([]byte("SSO auth URL not set or SSO server offline."))
//TODO: Use better looking template if exists
return false
}
//Check if the user have the cookie "Zoraxy-SSO" set
session, err := h.cookieStore.Get(r, "Zoraxy-SSO")
if err != nil {
//Redirect to auth subdomain
http.Redirect(w, r, redirectAuthURL+"/sso/login?m=new&t="+originalRequestURL, http.StatusFound)
return false
}
//Check if the user is logged in
if session.Values["username"] != true {
//Redirect to auth subdomain
http.Redirect(w, r, redirectAuthURL+"/sso/login?m=expired&t="+originalRequestURL, http.StatusFound)
return false
}
//Check if the current request subdomain is allowed
userName := session.Values["username"].(string)
user, err := h.GetSSOUser(userName)
if err != nil {
//User might have been removed from SSO. Redirect to auth subdomain
http.Redirect(w, r, redirectAuthURL, http.StatusFound)
return false
}
//Check if the user have access to the current subdomain
if !user.Subdomains[r.Host].AllowAccess {
//User is not allowed to access the current subdomain. Sent 403
http.Error(w, "Forbidden", http.StatusForbidden)
//TODO: Use better looking template if exists
return false
}
//User is logged in, continue to the next handler
return true
}
// Log a message with the SSO module tag
func (h *SSOHandler) Log(message string, err error) {
h.Config.Logger.PrintAndLog("SSO", message, err)
}

View File

@ -1,33 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Auth</title>
<link
rel="stylesheet"
href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
/>
<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<div class="jumbotron">
<form action="/oauth2/authorize" method="POST">
<h1>Authorize</h1>
<p>The client would like to perform actions on your behalf.</p>
<p>
<button
type="submit"
class="btn btn-primary btn-lg"
style="width:200px;"
>
Allow
</button>
</p>
</form>
</div>
</div>
</body>
</html>

View File

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Login Page</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
</head>
<body>
<div class="ui container">
<div class="ui middle aligned center aligned grid">
<div class="column">
<h2 class="ui teal image header">
<div class="content">
Log in to your account
</div>
</h2>
<form class="ui large form">
<div class="ui stacked segment">
<div class="field">
<div class="ui left icon input">
<i class="user icon"></i>
<input type="text" name="username" placeholder="Username">
</div>
</div>
<div class="field">
<div class="ui left icon input">
<i class="lock icon"></i>
<input type="password" name="password" placeholder="Password">
</div>
</div>
<div class="ui fluid large teal submit button">Login</div>
</div>
<div class="ui error message"></div>
</form>
<div class="ui message">
New to us? <a href="#">Sign Up</a>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js"></script>
</body>
</html>

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<h1>Login In</h1>
<form action="/oauth2/login" method="POST">
<div class="form-group">
<label for="username">User Name</label>
<input type="text" class="form-control" name="username" required placeholder="Please enter your user name">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" placeholder="Please enter your password">
</div>
<button type="submit" class="btn btn-success">Login</button>
</form>
</div>
</body>
</html>

View File

@ -1,309 +0,0 @@
package sso
/*
userHandlers.go
Handlers for SSO user management
If you are looking for handlers that changes the settings
of the SSO portal (e.g. authURL or port), please refer to
handlers.go.
*/
import (
"encoding/json"
"errors"
"net/http"
"github.com/gofrs/uuid"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/utils"
)
// HandleAddUser handle the request to add a new user to the SSO system
func (s *SSOHandler) HandleAddUser(w http.ResponseWriter, r *http.Request) {
username, err := utils.PostPara(r, "username")
if err != nil {
utils.SendErrorResponse(w, "invalid username given")
return
}
password, err := utils.PostPara(r, "password")
if err != nil {
utils.SendErrorResponse(w, "invalid password given")
return
}
newUserId, err := uuid.NewV4()
if err != nil {
utils.SendErrorResponse(w, "failed to generate new user ID")
return
}
//Create a new user entry
thisUserEntry := UserEntry{
UserID: newUserId.String(),
Username: username,
PasswordHash: auth.Hash(password),
TOTPCode: "",
Enable2FA: false,
}
js, _ := json.Marshal(thisUserEntry)
//Create a new user in the database
err = s.Config.Database.Write("sso_users", newUserId.String(), string(js))
if err != nil {
utils.SendErrorResponse(w, "failed to create new user")
return
}
utils.SendOK(w)
}
// Edit user information, only accept change of username, password and enabled subdomain filed
func (s *SSOHandler) HandleEditUser(w http.ResponseWriter, r *http.Request) {
userID, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userID)) {
utils.SendErrorResponse(w, "user not found")
return
}
//Load the user entry from database
userEntry, err := s.GetSSOUser(userID)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return
}
//Update each of the fields if it is provided
username, err := utils.PostPara(r, "username")
if err == nil {
userEntry.Username = username
}
password, err := utils.PostPara(r, "password")
if err == nil {
userEntry.PasswordHash = auth.Hash(password)
}
//Update the user entry in the database
js, _ := json.Marshal(userEntry)
err = s.Config.Database.Write("sso_users", userID, string(js))
if err != nil {
utils.SendErrorResponse(w, "failed to update user entry")
return
}
utils.SendOK(w)
}
// HandleRemoveUser remove a user from the SSO system
func (s *SSOHandler) HandleRemoveUser(w http.ResponseWriter, r *http.Request) {
userID, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userID)) {
utils.SendErrorResponse(w, "user not found")
return
}
//Remove the user from the database
err = s.Config.Database.Delete("sso_users", userID)
if err != nil {
utils.SendErrorResponse(w, "failed to remove user")
return
}
utils.SendOK(w)
}
// HandleListUser list all users in the SSO system
func (s *SSOHandler) HandleListUser(w http.ResponseWriter, r *http.Request) {
ssoUsers, err := s.ListSSOUsers()
if err != nil {
utils.SendErrorResponse(w, "failed to list users")
return
}
js, _ := json.Marshal(ssoUsers)
utils.SendJSONResponse(w, string(js))
}
// HandleAddSubdomain add a subdomain to a user
func (s *SSOHandler) HandleAddSubdomain(w http.ResponseWriter, r *http.Request) {
userid, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userid)) {
utils.SendErrorResponse(w, "user not found")
return
}
UserEntry, err := s.GetSSOUser(userid)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return
}
subdomain, err := utils.PostPara(r, "subdomain")
if err != nil {
utils.SendErrorResponse(w, "invalid subdomain given")
return
}
allowAccess, err := utils.PostBool(r, "allow_access")
if err != nil {
utils.SendErrorResponse(w, "invalid allow access value given")
return
}
UserEntry.Subdomains[subdomain] = &SubdomainAccessRule{
Subdomain: subdomain,
AllowAccess: allowAccess,
}
err = UserEntry.Update()
if err != nil {
utils.SendErrorResponse(w, "failed to update user entry")
return
}
utils.SendOK(w)
}
// HandleRemoveSubdomain remove a subdomain from a user
func (s *SSOHandler) HandleRemoveSubdomain(w http.ResponseWriter, r *http.Request) {
userid, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userid)) {
utils.SendErrorResponse(w, "user not found")
return
}
UserEntry, err := s.GetSSOUser(userid)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return
}
subdomain, err := utils.PostPara(r, "subdomain")
if err != nil {
utils.SendErrorResponse(w, "invalid subdomain given")
return
}
delete(UserEntry.Subdomains, subdomain)
err = UserEntry.Update()
if err != nil {
utils.SendErrorResponse(w, "failed to update user entry")
return
}
utils.SendOK(w)
}
// HandleEnable2FA enable 2FA for a user
func (s *SSOHandler) HandleEnable2FA(w http.ResponseWriter, r *http.Request) {
userid, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userid)) {
utils.SendErrorResponse(w, "user not found")
return
}
UserEntry, err := s.GetSSOUser(userid)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return
}
UserEntry.Enable2FA = true
provisionUri, err := UserEntry.ResetTotp(UserEntry.UserID, "Zoraxy-SSO")
if err != nil {
utils.SendErrorResponse(w, "failed to reset TOTP")
return
}
//As the ResetTotp function will update the user entry in the database, no need to call Update here
js, _ := json.Marshal(provisionUri)
utils.SendJSONResponse(w, string(js))
}
// Handle Disable 2FA for a user
func (s *SSOHandler) HandleDisable2FA(w http.ResponseWriter, r *http.Request) {
userid, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userid)) {
utils.SendErrorResponse(w, "user not found")
return
}
UserEntry, err := s.GetSSOUser(userid)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return
}
UserEntry.Enable2FA = false
UserEntry.TOTPCode = ""
err = UserEntry.Update()
if err != nil {
utils.SendErrorResponse(w, "failed to update user entry")
return
}
utils.SendOK(w)
}
// HandleVerify2FA verify the 2FA code for a user
func (s *SSOHandler) HandleVerify2FA(w http.ResponseWriter, r *http.Request) (bool, error) {
userid, err := utils.PostPara(r, "user_id")
if err != nil {
return false, errors.New("invalid user ID given")
}
if !(s.SSOUserExists(userid)) {
utils.SendErrorResponse(w, "user not found")
return false, errors.New("user not found")
}
UserEntry, err := s.GetSSOUser(userid)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return false, errors.New("failed to load user entry")
}
totpCode, _ := utils.PostPara(r, "totp_code")
if !UserEntry.Enable2FA {
//If 2FA is not enabled, return true
return true, nil
}
if !UserEntry.VerifyTotp(totpCode) {
return false, nil
}
return true, nil
}

View File

@ -1,141 +0,0 @@
package sso
import (
"encoding/json"
"time"
"github.com/xlzd/gotp"
"imuslab.com/zoraxy/mod/auth"
)
/*
users.go
This file contains the user structure and user management
functions for the SSO module.
If you are looking for handlers, please refer to handlers.go.
*/
type SubdomainAccessRule struct {
Subdomain string
AllowAccess bool
}
type UserEntry struct {
UserID string `json:sub` //User ID
Username string `json:"name"` //Username
Email string `json:"email"` //Email
PasswordHash string `json:"passwordhash"` //Password hash
TOTPCode string `json:"totpcode"` //TOTP code
Enable2FA bool `json:"enable2fa"` //Enable 2FA
Subdomains map[string]*SubdomainAccessRule `json:"subdomains"` //Subdomain access rules
LastLogin int64 `json:"lastlogin"` //Last login time
LastLoginIP string `json:"lastloginip"` //Last login IP
LastLoginCountry string `json:"lastlogincountry"` //Last login country
parent *SSOHandler //Parent SSO handler
}
type ClientResponse struct {
Sub string `json:"sub"` //User ID
Name string `json:"name"` //Username
Nickname string `json:"nickname"` //Nickname
PreferredUsername string `json:"preferred_username"` //Preferred Username
Email string `json:"email"` //Email
Locale string `json:"locale"` //Locale
Website string `json:"website"` //Website
}
func (s *SSOHandler) SSOUserExists(userid string) bool {
//Check if the user exists in the database
var userEntry UserEntry
err := s.Config.Database.Read("sso_users", userid, &userEntry)
return err == nil
}
func (s *SSOHandler) GetSSOUser(userid string) (UserEntry, error) {
//Load the user entry from database
var userEntry UserEntry
err := s.Config.Database.Read("sso_users", userid, &userEntry)
if err != nil {
return UserEntry{}, err
}
userEntry.parent = s
return userEntry, nil
}
func (s *SSOHandler) ListSSOUsers() ([]*UserEntry, error) {
entries, err := s.Config.Database.ListTable("sso_users")
if err != nil {
return nil, err
}
ssoUsers := []*UserEntry{}
for _, keypairs := range entries {
group := new(UserEntry)
json.Unmarshal(keypairs[1], &group)
group.parent = s
ssoUsers = append(ssoUsers, group)
}
return ssoUsers, nil
}
// Validate the username and password
func (s *SSOHandler) ValidateUsernameAndPassword(username string, password string) bool {
//Validate the username and password
var userEntry UserEntry
err := s.Config.Database.Read("sso_users", username, &userEntry)
if err != nil {
return false
}
//TODO: Remove after testing
if (username == "test") && (password == "test") {
return true
}
return userEntry.VerifyPassword(password)
}
func (s *UserEntry) VerifyPassword(password string) bool {
return s.PasswordHash == auth.Hash(password)
}
// Write changes in the user entry back to the database
func (u *UserEntry) Update() error {
js, _ := json.Marshal(u)
err := u.parent.Config.Database.Write("sso_users", u.UserID, string(js))
if err != nil {
return err
}
return nil
}
// Reset and update the TOTP code for the current user
// Return the provision uri of the new TOTP code for Google Authenticator
func (u *UserEntry) ResetTotp(accountName string, issuerName string) (string, error) {
u.TOTPCode = gotp.RandomSecret(16)
totp := gotp.NewDefaultTOTP(u.TOTPCode)
err := u.Update()
if err != nil {
return "", err
}
return totp.ProvisioningUri(accountName, issuerName), nil
}
// Verify the TOTP code at current time
func (u *UserEntry) VerifyTotp(enteredCode string) bool {
totp := gotp.NewDefaultTOTP(u.TOTPCode)
return totp.Verify(enteredCode, time.Now().Unix())
}
func (u *UserEntry) GetClientResponse() ClientResponse {
return ClientResponse{
Sub: u.UserID,
Name: u.Username,
Nickname: u.Username,
PreferredUsername: u.Username,
Email: u.Email,
Locale: "en",
Website: "",
}
}

View File

@ -9,17 +9,39 @@ package database
*/ */
import ( import (
"sync" "log"
"runtime"
"imuslab.com/zoraxy/mod/database/dbinc"
) )
type Database struct { type Database struct {
Db interface{} //This will be nil on openwrt and *bolt.DB in the rest of the systems Db interface{} //This will be nil on openwrt, leveldb.DB on x64 platforms or bolt.DB on other platforms
Tables sync.Map BackendType dbinc.BackendType
ReadOnly bool Backend dbinc.Backend
} }
func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) { func NewDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
return newDatabase(dbfile, readOnlyMode) if runtime.GOARCH == "riscv64" {
log.Println("RISCV hardware detected, ignoring the backend type and using FS emulated database")
}
return newDatabase(dbfile, backendType)
}
// Get the recommended backend type for the current system
func GetRecommendedBackendType() dbinc.BackendType {
//Check if the system is running on RISCV hardware
if runtime.GOARCH == "riscv64" {
//RISCV hardware, currently only support FS emulated database
return dbinc.BackendFSOnly
} else if runtime.GOOS == "windows" || (runtime.GOOS == "linux" && runtime.GOARCH == "amd64") {
//Powerful hardware
return dbinc.BackendBoltDB
//return dbinc.BackendLevelDB
}
//Default to BoltDB, the safest option
return dbinc.BackendBoltDB
} }
/* /*
@ -29,15 +51,6 @@ func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
err := sysdb.DropTable("MyTable") err := sysdb.DropTable("MyTable")
*/ */
func (d *Database) UpdateReadWriteMode(readOnly bool) {
d.ReadOnly = readOnly
}
//Dump the whole db into a log file
func (d *Database) Dump(filename string) ([]string, error) {
return d.dump(filename)
}
// Create a new table // Create a new table
func (d *Database) NewTable(tableName string) error { func (d *Database) NewTable(tableName string) error {
return d.newTable(tableName) return d.newTable(tableName)
@ -55,12 +68,15 @@ func (d *Database) DropTable(tableName string) error {
/* /*
Write to database with given tablename and key. Example Usage: Write to database with given tablename and key. Example Usage:
type demo struct{ type demo struct{
content string content string
} }
thisDemo := demo{ thisDemo := demo{
content: "Hello World", content: "Hello World",
} }
err := sysdb.Write("MyTable", "username/message",thisDemo); err := sysdb.Write("MyTable", "username/message",thisDemo);
*/ */
func (d *Database) Write(tableName string, key string, value interface{}) error { func (d *Database) Write(tableName string, key string, value interface{}) error {
@ -81,6 +97,13 @@ func (d *Database) Read(tableName string, key string, assignee interface{}) erro
return d.read(tableName, key, assignee) return d.read(tableName, key, assignee)
} }
/*
Check if a key exists in the database table given tablename and key
if sysdb.KeyExists("MyTable", "username/message"){
log.Println("Key exists")
}
*/
func (d *Database) KeyExists(tableName string, key string) bool { func (d *Database) KeyExists(tableName string, key string) bool {
return d.keyExists(tableName, key) return d.keyExists(tableName, key)
} }
@ -115,6 +138,9 @@ func (d *Database) ListTable(tableName string) ([][][]byte, error) {
return d.listTable(tableName) return d.listTable(tableName)
} }
/*
Close the database connection
*/
func (d *Database) Close() { func (d *Database) Close() {
d.close() d.close()
} }

View File

@ -4,183 +4,67 @@
package database package database
import ( import (
"encoding/json"
"errors" "errors"
"log"
"sync"
"github.com/boltdb/bolt" "imuslab.com/zoraxy/mod/database/dbbolt"
"imuslab.com/zoraxy/mod/database/dbinc"
"imuslab.com/zoraxy/mod/database/dbleveldb"
) )
func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) { func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
db, err := bolt.Open(dbfile, 0600, nil) if backendType == dbinc.BackendFSOnly {
if err != nil { return nil, errors.New("Unsupported backend type for this platform")
return nil, err
} }
tableMap := sync.Map{} if backendType == dbinc.BackendLevelDB {
//Build the table list from database db, err := dbleveldb.NewDB(dbfile)
err = db.View(func(tx *bolt.Tx) error {
return tx.ForEach(func(name []byte, _ *bolt.Bucket) error {
tableMap.Store(string(name), "")
return nil
})
})
return &Database{ return &Database{
Db: db, Db: nil,
Tables: tableMap, BackendType: backendType,
ReadOnly: readOnlyMode, Backend: db,
}, err }, err
} }
//Dump the whole db into a log file db, err := dbbolt.NewBoltDatabase(dbfile)
func (d *Database) dump(filename string) ([]string, error) { return &Database{
results := []string{} Db: nil,
BackendType: backendType,
d.Tables.Range(func(tableName, v interface{}) bool { Backend: db,
entries, err := d.ListTable(tableName.(string)) }, err
if err != nil {
log.Println("Reading table " + tableName.(string) + " failed: " + err.Error())
return false
}
for _, keypairs := range entries {
results = append(results, string(keypairs[0])+":"+string(keypairs[1])+"\n")
}
return true
})
return results, nil
} }
//Create a new table
func (d *Database) newTable(tableName string) error { func (d *Database) newTable(tableName string) error {
if d.ReadOnly == true { return d.Backend.NewTable(tableName)
return errors.New("Operation rejected in ReadOnly mode")
} }
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
return nil
})
d.Tables.Store(tableName, "")
return err
}
//Check is table exists
func (d *Database) tableExists(tableName string) bool { func (d *Database) tableExists(tableName string) bool {
if _, ok := d.Tables.Load(tableName); ok { return d.Backend.TableExists(tableName)
return true
}
return false
} }
//Drop the given table
func (d *Database) dropTable(tableName string) error { func (d *Database) dropTable(tableName string) error {
if d.ReadOnly == true { return d.Backend.DropTable(tableName)
return errors.New("Operation rejected in ReadOnly mode")
} }
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
err := tx.DeleteBucket([]byte(tableName))
if err != nil {
return err
}
return nil
})
return err
}
//Write to table
func (d *Database) write(tableName string, key string, value interface{}) error { func (d *Database) write(tableName string, key string, value interface{}) error {
if d.ReadOnly { return d.Backend.Write(tableName, key, value)
return errors.New("Operation rejected in ReadOnly mode")
}
jsonString, err := json.Marshal(value)
if err != nil {
return err
}
err = d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
b := tx.Bucket([]byte(tableName))
err = b.Put([]byte(key), jsonString)
return err
})
return err
} }
func (d *Database) read(tableName string, key string, assignee interface{}) error { func (d *Database) read(tableName string, key string, assignee interface{}) error {
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error { return d.Backend.Read(tableName, key, assignee)
b := tx.Bucket([]byte(tableName))
v := b.Get([]byte(key))
json.Unmarshal(v, &assignee)
return nil
})
return err
} }
func (d *Database) keyExists(tableName string, key string) bool { func (d *Database) keyExists(tableName string, key string) bool {
resultIsNil := false return d.Backend.KeyExists(tableName, key)
if !d.TableExists(tableName) {
//Table not exists. Do not proceed accessing key
log.Println("[DB] ERROR: Requesting key from table that didn't exist!!!")
return false
}
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
v := b.Get([]byte(key))
if v == nil {
resultIsNil = true
}
return nil
})
if err != nil {
return false
} else {
if resultIsNil {
return false
} else {
return true
}
}
} }
func (d *Database) delete(tableName string, key string) error { func (d *Database) delete(tableName string, key string) error {
if d.ReadOnly { return d.Backend.Delete(tableName, key)
return errors.New("Operation rejected in ReadOnly mode")
}
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
tx.Bucket([]byte(tableName)).Delete([]byte(key))
return nil
})
return err
} }
func (d *Database) listTable(tableName string) ([][][]byte, error) { func (d *Database) listTable(tableName string) ([][][]byte, error) {
var results [][][]byte return d.Backend.ListTable(tableName)
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
results = append(results, [][]byte{k, v})
}
return nil
})
return results, err
} }
func (d *Database) close() { func (d *Database) close() {
d.Db.(*bolt.DB).Close() d.Backend.Close()
} }

View File

@ -10,10 +10,19 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"imuslab.com/zoraxy/mod/database/dbinc"
) )
func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) { /*
OpenWRT or RISCV backend
For OpenWRT or RISCV platform, we will use the filesystem as the database backend
as boltdb or leveldb is not supported on these platforms, including boltDB and LevelDB
in conditional compilation will create a build error on these platforms
*/
func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
dbRootPath := filepath.ToSlash(filepath.Clean(dbfile)) dbRootPath := filepath.ToSlash(filepath.Clean(dbfile))
dbRootPath = "fsdb/" + dbRootPath dbRootPath = "fsdb/" + dbRootPath
err := os.MkdirAll(dbRootPath, 0755) err := os.MkdirAll(dbRootPath, 0755)
@ -21,24 +30,11 @@ func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
return nil, err return nil, err
} }
tableMap := sync.Map{}
//build the table list from file system
files, err := filepath.Glob(filepath.Join(dbRootPath, "/*"))
if err != nil {
return nil, err
}
for _, file := range files {
if isDirectory(file) {
tableMap.Store(filepath.Base(file), "")
}
}
log.Println("Filesystem Emulated Key-value Database Service Started: " + dbRootPath) log.Println("Filesystem Emulated Key-value Database Service Started: " + dbRootPath)
return &Database{ return &Database{
Db: dbRootPath, Db: dbRootPath,
Tables: tableMap, BackendType: dbinc.BackendFSOnly,
ReadOnly: readOnlyMode, Backend: nil,
}, nil }, nil
} }
@ -61,9 +57,7 @@ func (d *Database) dump(filename string) ([]string, error) {
} }
func (d *Database) newTable(tableName string) error { func (d *Database) newTable(tableName string) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
if !fileExists(tablePath) { if !fileExists(tablePath) {
return os.MkdirAll(tablePath, 0755) return os.MkdirAll(tablePath, 0755)
@ -85,9 +79,7 @@ func (d *Database) tableExists(tableName string) bool {
} }
func (d *Database) dropTable(tableName string) error { func (d *Database) dropTable(tableName string) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
if d.tableExists(tableName) { if d.tableExists(tableName) {
return os.RemoveAll(tablePath) return os.RemoveAll(tablePath)
@ -98,9 +90,7 @@ func (d *Database) dropTable(tableName string) error {
} }
func (d *Database) write(tableName string, key string, value interface{}) error { func (d *Database) write(tableName string, key string, value interface{}) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
js, err := json.Marshal(value) js, err := json.Marshal(value)
if err != nil { if err != nil {
@ -138,9 +128,7 @@ func (d *Database) keyExists(tableName string, key string) bool {
} }
func (d *Database) delete(tableName string, key string) error { func (d *Database) delete(tableName string, key string) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
if !d.keyExists(tableName, key) { if !d.keyExists(tableName, key) {
return errors.New("key not exists") return errors.New("key not exists")
} }

View File

@ -0,0 +1,141 @@
package dbbolt
import (
"encoding/json"
"errors"
"github.com/boltdb/bolt"
)
type Database struct {
Db interface{} //This is the bolt database object
}
func NewBoltDatabase(dbfile string) (*Database, error) {
db, err := bolt.Open(dbfile, 0600, nil)
if err != nil {
return nil, err
}
return &Database{
Db: db,
}, err
}
// Create a new table
func (d *Database) NewTable(tableName string) error {
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
return nil
})
return err
}
// Check is table exists
func (d *Database) TableExists(tableName string) bool {
return d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
if b == nil {
return errors.New("table not exists")
}
return nil
}) == nil
}
// Drop the given table
func (d *Database) DropTable(tableName string) error {
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
err := tx.DeleteBucket([]byte(tableName))
if err != nil {
return err
}
return nil
})
return err
}
// Write to table
func (d *Database) Write(tableName string, key string, value interface{}) error {
jsonString, err := json.Marshal(value)
if err != nil {
return err
}
err = d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
b := tx.Bucket([]byte(tableName))
err = b.Put([]byte(key), jsonString)
return err
})
return err
}
func (d *Database) Read(tableName string, key string, assignee interface{}) error {
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
v := b.Get([]byte(key))
json.Unmarshal(v, &assignee)
return nil
})
return err
}
func (d *Database) KeyExists(tableName string, key string) bool {
resultIsNil := false
if !d.TableExists(tableName) {
//Table not exists. Do not proceed accessing key
//log.Println("[DB] ERROR: Requesting key from table that didn't exist!!!")
return false
}
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
v := b.Get([]byte(key))
if v == nil {
resultIsNil = true
}
return nil
})
if err != nil {
return false
} else {
if resultIsNil {
return false
} else {
return true
}
}
}
func (d *Database) Delete(tableName string, key string) error {
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
tx.Bucket([]byte(tableName)).Delete([]byte(key))
return nil
})
return err
}
func (d *Database) ListTable(tableName string) ([][][]byte, error) {
var results [][][]byte
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
results = append(results, [][]byte{k, v})
}
return nil
})
return results, err
}
func (d *Database) Close() {
d.Db.(*bolt.DB).Close()
}

View File

@ -0,0 +1,67 @@
package dbbolt_test
import (
"os"
"testing"
"imuslab.com/zoraxy/mod/database/dbbolt"
)
func TestNewBoltDatabase(t *testing.T) {
dbfile := "test.db"
defer os.Remove(dbfile)
db, err := dbbolt.NewBoltDatabase(dbfile)
if err != nil {
t.Fatalf("Failed to create new Bolt database: %v", err)
}
defer db.Close()
if db.Db == nil {
t.Fatalf("Expected non-nil database object")
}
}
func TestNewTable(t *testing.T) {
dbfile := "test.db"
defer os.Remove(dbfile)
db, err := dbbolt.NewBoltDatabase(dbfile)
if err != nil {
t.Fatalf("Failed to create new Bolt database: %v", err)
}
defer db.Close()
err = db.NewTable("testTable")
if err != nil {
t.Fatalf("Failed to create new table: %v", err)
}
}
func TestTableExists(t *testing.T) {
dbfile := "test.db"
defer os.Remove(dbfile)
db, err := dbbolt.NewBoltDatabase(dbfile)
if err != nil {
t.Fatalf("Failed to create new Bolt database: %v", err)
}
defer db.Close()
tableName := "testTable"
err = db.NewTable(tableName)
if err != nil {
t.Fatalf("Failed to create new table: %v", err)
}
exists := db.TableExists(tableName)
if !exists {
t.Fatalf("Expected table %s to exist", tableName)
}
nonExistentTable := "nonExistentTable"
exists = db.TableExists(nonExistentTable)
if exists {
t.Fatalf("Expected table %s to not exist", nonExistentTable)
}
}

View File

@ -0,0 +1,39 @@
package dbinc
/*
dbinc is the interface for all database backend
*/
type BackendType int
const (
BackendBoltDB BackendType = iota //Default backend
BackendFSOnly //OpenWRT or RISCV backend
BackendLevelDB //LevelDB backend
BackEndAuto = BackendBoltDB
)
type Backend interface {
NewTable(tableName string) error
TableExists(tableName string) bool
DropTable(tableName string) error
Write(tableName string, key string, value interface{}) error
Read(tableName string, key string, assignee interface{}) error
KeyExists(tableName string, key string) bool
Delete(tableName string, key string) error
ListTable(tableName string) ([][][]byte, error)
Close()
}
func (b BackendType) String() string {
switch b {
case BackendBoltDB:
return "BoltDB"
case BackendFSOnly:
return "File System Emulated Key-Value Store"
case BackendLevelDB:
return "LevelDB"
default:
return "Unknown"
}
}

View File

@ -0,0 +1,152 @@
package dbleveldb
import (
"encoding/json"
"log"
"path/filepath"
"strings"
"sync"
"time"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
"imuslab.com/zoraxy/mod/database/dbinc"
)
// Ensure the DB struct implements the Backend interface
var _ dbinc.Backend = (*DB)(nil)
type DB struct {
db *leveldb.DB
Table sync.Map //For emulating table creation
batch leveldb.Batch //Batch write
writeFlushTicker *time.Ticker //Ticker for flushing data into disk
writeFlushStop chan bool //Stop channel for write flush ticker
}
func NewDB(path string) (*DB, error) {
//If the path is not a directory (e.g. /tmp/dbfile.db), convert the filename to directory
if filepath.Ext(path) != "" {
path = strings.ReplaceAll(path, ".", "_")
}
db, err := leveldb.OpenFile(path, nil)
if err != nil {
return nil, err
}
thisDB := &DB{
db: db,
Table: sync.Map{},
batch: leveldb.Batch{},
}
//Create a ticker to flush data into disk every 1 seconds
writeFlushTicker := time.NewTicker(1 * time.Second)
writeFlushStop := make(chan bool)
go func() {
for {
select {
case <-writeFlushTicker.C:
if thisDB.batch.Len() == 0 {
//No flushing needed
continue
}
err = db.Write(&thisDB.batch, nil)
if err != nil {
log.Println("[LevelDB] Failed to flush data into disk: ", err)
}
thisDB.batch.Reset()
case <-writeFlushStop:
return
}
}
}()
thisDB.writeFlushTicker = writeFlushTicker
thisDB.writeFlushStop = writeFlushStop
return thisDB, nil
}
func (d *DB) NewTable(tableName string) error {
//Create a table entry in the sync.Map
d.Table.Store(tableName, true)
return nil
}
func (d *DB) TableExists(tableName string) bool {
_, ok := d.Table.Load(tableName)
return ok
}
func (d *DB) DropTable(tableName string) error {
d.Table.Delete(tableName)
iter := d.db.NewIterator(nil, nil)
defer iter.Release()
for iter.Next() {
key := iter.Key()
if filepath.Dir(string(key)) == tableName {
err := d.db.Delete(key, nil)
if err != nil {
return err
}
}
}
return nil
}
func (d *DB) Write(tableName string, key string, value interface{}) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
d.batch.Put([]byte(filepath.ToSlash(filepath.Join(tableName, key))), data)
return nil
}
func (d *DB) Read(tableName string, key string, assignee interface{}) error {
data, err := d.db.Get([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
if err != nil {
return err
}
return json.Unmarshal(data, assignee)
}
func (d *DB) KeyExists(tableName string, key string) bool {
_, err := d.db.Get([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
return err == nil
}
func (d *DB) Delete(tableName string, key string) error {
return d.db.Delete([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
}
func (d *DB) ListTable(tableName string) ([][][]byte, error) {
iter := d.db.NewIterator(util.BytesPrefix([]byte(tableName+"/")), nil)
defer iter.Release()
var result [][][]byte
for iter.Next() {
key := iter.Key()
//The key contains the table name as prefix. Trim it before returning
value := iter.Value()
result = append(result, [][]byte{[]byte(strings.TrimPrefix(string(key), tableName+"/")), value})
}
err := iter.Error()
if err != nil {
return nil, err
}
return result, nil
}
func (d *DB) Close() {
//Write the remaining data in batch back into disk
d.writeFlushStop <- true
d.writeFlushTicker.Stop()
d.db.Write(&d.batch, nil)
d.db.Close()
}

View File

@ -0,0 +1,141 @@
package dbleveldb_test
import (
"os"
"testing"
"imuslab.com/zoraxy/mod/database/dbleveldb"
)
func TestNewDB(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
}
func TestNewTable(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
err = db.NewTable("testTable")
if err != nil {
t.Fatalf("Failed to create new table: %v", err)
}
}
func TestTableExists(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
db.NewTable("testTable")
if !db.TableExists("testTable") {
t.Fatalf("Table should exist")
}
}
func TestDropTable(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
db.NewTable("testTable")
err = db.DropTable("testTable")
if err != nil {
t.Fatalf("Failed to drop table: %v", err)
}
if db.TableExists("testTable") {
t.Fatalf("Table should not exist")
}
}
func TestWriteAndRead(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
db.NewTable("testTable")
err = db.Write("testTable", "testKey", "testValue")
if err != nil {
t.Fatalf("Failed to write to table: %v", err)
}
var value string
err = db.Read("testTable", "testKey", &value)
if err != nil {
t.Fatalf("Failed to read from table: %v", err)
}
if value != "testValue" {
t.Fatalf("Expected 'testValue', got '%v'", value)
}
}
func TestListTable(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
db.NewTable("testTable")
err = db.Write("testTable", "testKey1", "testValue1")
if err != nil {
t.Fatalf("Failed to write to table: %v", err)
}
err = db.Write("testTable", "testKey2", "testValue2")
if err != nil {
t.Fatalf("Failed to write to table: %v", err)
}
result, err := db.ListTable("testTable")
if err != nil {
t.Fatalf("Failed to list table: %v", err)
}
if len(result) != 2 {
t.Fatalf("Expected 2 entries, got %v", len(result))
}
expected := map[string]string{
"testTable/testKey1": "\"testValue1\"",
"testTable/testKey2": "\"testValue2\"",
}
for _, entry := range result {
key := string(entry[0])
value := string(entry[1])
if expected[key] != value {
t.Fatalf("Expected value '%v' for key '%v', got '%v'", expected[key], key, value)
}
}
}

View File

@ -83,23 +83,12 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} }
//SSO Interception Mode
if sep.UseSSOIntercept {
allowPass := h.Parent.Option.SSOHandler.ServeForwardAuth(w, r)
if !allowPass {
h.Parent.Option.Logger.LogHTTPRequest(r, "sso-x", 307)
return
}
}
//Validate basic auth //Validate basic auth
if sep.RequireBasicAuth { respWritten := handleAuthProviderRouting(sep, w, r, h)
err := h.handleBasicAuthRouting(w, r, sep) if respWritten {
if err != nil { //Request handled by subroute
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401)
return return
} }
}
//Check if any virtual directory rules matches //Check if any virtual directory rules matches
proxyingPath := strings.TrimSpace(r.RequestURI) proxyingPath := strings.TrimSpace(r.RequestURI)
@ -108,7 +97,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
//Virtual directory routing rule found. Route via vdir mode //Virtual directory routing rule found. Route via vdir mode
h.vdirRequest(w, r, targetProxyEndpoint) h.vdirRequest(w, r, targetProxyEndpoint)
return return
} else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyType_Root { } else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyTypeRoot {
potentialProxtEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/") potentialProxtEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil && !potentialProxtEndpoint.Disabled { if potentialProxtEndpoint != nil && !potentialProxtEndpoint.Disabled {
//Missing tailing slash. Redirect to target proxy endpoint //Missing tailing slash. Redirect to target proxy endpoint
@ -153,7 +142,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/* /*
handleRootRouting handleRootRouting
This function handle root routing situations where there are no subdomain This function handle root routing (aka default sites) situations where there are no subdomain
, vdir or special routing rule matches the requested URI. , vdir or special routing rule matches the requested URI.
Once entered this routing segment, the root routing options will take over Once entered this routing segment, the root routing options will take over
@ -180,7 +169,7 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
//Virtual directory routing rule found. Route via vdir mode //Virtual directory routing rule found. Route via vdir mode
h.vdirRequest(w, r, targetProxyEndpoint) h.vdirRequest(w, r, targetProxyEndpoint)
return return
} else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyType_Root { } else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyTypeRoot {
potentialProxtEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/") potentialProxtEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled { if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
//Missing tailing slash. Redirect to target proxy endpoint //Missing tailing slash. Redirect to target proxy endpoint
@ -228,5 +217,25 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
} else { } else {
w.Write(template) w.Write(template)
} }
case DefaultSite_NoResponse:
//No response. Just close the connection
h.Parent.logRequest(r, false, 444, "root-noresponse", domainOnly)
hijacker, ok := w.(http.Hijacker)
if !ok {
return
}
conn, _, err := hijacker.Hijack()
if err != nil {
return
}
conn.Close()
case DefaultSite_TeaPot:
//I'm a teapot
h.Parent.logRequest(r, false, 418, "root-teapot", domainOnly)
http.Error(w, "I'm a teapot", http.StatusTeapot)
default:
//Unknown routing option. Send empty response
h.Parent.logRequest(r, false, 544, "root-unknown", domainOnly)
http.Error(w, "544 - No Route Defined", 544)
} }
} }

View File

@ -0,0 +1,108 @@
package dynamicproxy
import (
"errors"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/auth"
)
/*
authProviders.go
This script handle authentication providers
*/
/*
Central Authentication Provider Router
This function will route the request to the correct authentication provider
if the return value is true, do not continue to the next handler
handleAuthProviderRouting takes in 4 parameters:
- sep: the ProxyEndpoint object
- w: the http.ResponseWriter object
- r: the http.Request object
- h: the ProxyHandler object
and return a boolean indicate if the request is written to http.ResponseWriter
- true: the request is handled, do not write to http.ResponseWriter
- false: the request is not handled (usually means auth ok), continue to the next handler
*/
func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *http.Request, h *ProxyHandler) bool {
if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic {
err := h.handleBasicAuthRouting(w, r, sep)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401)
return true
}
} else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthelia {
err := h.handleAutheliaAuth(w, r)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401)
return true
}
}
//No authentication provider, do not need to handle
return false
}
/* Basic Auth */
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
err := handleBasicAuth(w, r, pe)
if err != nil {
h.Parent.logRequest(r, false, 401, "host", r.URL.Hostname())
}
return err
}
// Handle basic auth logic
// do not write to http.ResponseWriter if err return is not nil (already handled by this function)
func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
if len(pe.AuthenticationProvider.BasicAuthExceptionRules) > 0 {
//Check if the current path matches the exception rules
for _, exceptionRule := range pe.AuthenticationProvider.BasicAuthExceptionRules {
if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) {
//This path is excluded from basic auth
return nil
}
}
}
u, p, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
//Check for the credentials to see if there is one matching
hashedPassword := auth.Hash(p)
matchingFound := false
for _, cred := range pe.AuthenticationProvider.BasicAuthCredentials {
if u == cred.Username && hashedPassword == cred.PasswordHash {
matchingFound = true
//Set the X-Remote-User header
r.Header.Set("X-Remote-User", u)
break
}
}
if !matchingFound {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
return nil
}
/* Authelia */
// Handle authelia auth routing
func (h *ProxyHandler) handleAutheliaAuth(w http.ResponseWriter, r *http.Request) error {
return h.Parent.Option.AutheliaRouter.HandleAutheliaAuth(w, r)
}

View File

@ -1,66 +0,0 @@
package dynamicproxy
import (
"errors"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/auth"
)
/*
BasicAuth.go
This file handles the basic auth on proxy endpoints
if RequireBasicAuth is set to true
*/
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
err := handleBasicAuth(w, r, pe)
if err != nil {
h.Parent.logRequest(r, false, 401, "host", r.URL.Hostname())
}
return err
}
// Handle basic auth logic
// do not write to http.ResponseWriter if err return is not nil (already handled by this function)
func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
if len(pe.BasicAuthExceptionRules) > 0 {
//Check if the current path matches the exception rules
for _, exceptionRule := range pe.BasicAuthExceptionRules {
if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) {
//This path is excluded from basic auth
return nil
}
}
}
u, p, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
//Check for the credentials to see if there is one matching
hashedPassword := auth.Hash(p)
matchingFound := false
for _, cred := range pe.BasicAuthCredentials {
if u == cred.Username && hashedPassword == cred.PasswordHash {
matchingFound = true
//Set the X-Remote-User header
r.Header.Set("X-Remote-User", u)
break
}
}
if !matchingFound {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
return nil
}

View File

@ -144,7 +144,7 @@ func (router *Router) StartProxyService() error {
} }
//Validate basic auth //Validate basic auth
if sep.RequireBasicAuth { if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic {
err := handleBasicAuth(w, r, sep) err := handleBasicAuth(w, r, sep)
if err != nil { if err != nil {
return return
@ -161,8 +161,8 @@ func (router *Router) StartProxyService() error {
ProxyDomain: selectedUpstream.OriginIpOrDomain, ProxyDomain: selectedUpstream.OriginIpOrDomain,
OriginalHost: originalHostHeader, OriginalHost: originalHostHeader,
UseTLS: selectedUpstream.RequireTLS, UseTLS: selectedUpstream.RequireTLS,
HostHeaderOverwrite: sep.RequestHostOverwrite, HostHeaderOverwrite: sep.HeaderRewriteRules.RequestHostOverwrite,
NoRemoveHopByHop: sep.DisableHopByHopHeaderRemoval, NoRemoveHopByHop: sep.HeaderRewriteRules.DisableHopByHopHeaderRemoval,
PathPrefix: "", PathPrefix: "",
Version: sep.parent.Option.HostVersion, Version: sep.parent.Option.HostVersion,
}) })

View File

@ -27,7 +27,7 @@ import (
// Check if a user define header exists in this endpoint, ignore case // Check if a user define header exists in this endpoint, ignore case
func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool { func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
for _, header := range ep.UserDefinedHeaders { for _, header := range ep.HeaderRewriteRules.UserDefinedHeaders {
if strings.EqualFold(header.Key, key) { if strings.EqualFold(header.Key, key) {
return true return true
} }
@ -38,13 +38,13 @@ func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
// Remvoe a user defined header from the list // Remvoe a user defined header from the list
func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error { func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
newHeaderList := []*rewrite.UserDefinedHeader{} newHeaderList := []*rewrite.UserDefinedHeader{}
for _, header := range ep.UserDefinedHeaders { for _, header := range ep.HeaderRewriteRules.UserDefinedHeaders {
if !strings.EqualFold(header.Key, key) { if !strings.EqualFold(header.Key, key) {
newHeaderList = append(newHeaderList, header) newHeaderList = append(newHeaderList, header)
} }
} }
ep.UserDefinedHeaders = newHeaderList ep.HeaderRewriteRules.UserDefinedHeaders = newHeaderList
return nil return nil
} }
@ -56,7 +56,7 @@ func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *rewrite.UserDefined
} }
newHeaderRule.Key = cases.Title(language.Und, cases.NoLower).String(newHeaderRule.Key) newHeaderRule.Key = cases.Title(language.Und, cases.NoLower).String(newHeaderRule.Key)
ep.UserDefinedHeaders = append(ep.UserDefinedHeaders, newHeaderRule) ep.HeaderRewriteRules.UserDefinedHeaders = append(ep.HeaderRewriteRules.UserDefinedHeaders, newHeaderRule)
return nil return nil
} }
@ -123,9 +123,9 @@ func (ep *ProxyEndpoint) AddVirtualDirectoryRule(vdir *VirtualDirectoryEndpoint)
return nil, err return nil, err
} }
if ep.ProxyType == ProxyType_Root { if ep.ProxyType == ProxyTypeRoot {
parentRouter.Root = readyRoutingRule parentRouter.Root = readyRoutingRule
} else if ep.ProxyType == ProxyType_Host { } else if ep.ProxyType == ProxyTypeHost {
ep.Remove() ep.Remove()
parentRouter.AddProxyRouteToRuntime(readyRoutingRule) parentRouter.AddProxyRouteToRuntime(readyRoutingRule)
} else { } else {
@ -264,5 +264,6 @@ func (ep *ProxyEndpoint) Remove() error {
// use prepare -> remove -> add if you change anything in the endpoint // use prepare -> remove -> add if you change anything in the endpoint
// that effects the proxy routing src / dest // that effects the proxy routing src / dest
func (ep *ProxyEndpoint) UpdateToRuntime() { func (ep *ProxyEndpoint) UpdateToRuntime() {
ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep) lookupHostname := strings.ToLower(ep.RootOrMatchingDomain)
ep.parent.ProxyEndpoints.Store(lookupHostname, ep)
} }

View File

@ -35,6 +35,7 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
// Get the proxy endpoint from hostname, which might includes checking of wildcard certificates // Get the proxy endpoint from hostname, which might includes checking of wildcard certificates
func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint { func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
var targetSubdomainEndpoint *ProxyEndpoint = nil var targetSubdomainEndpoint *ProxyEndpoint = nil
hostname = strings.ToLower(hostname)
ep, ok := router.ProxyEndpoints.Load(hostname) ep, ok := router.ProxyEndpoints.Load(hostname)
if ok { if ok {
//Exact hit //Exact hit
@ -145,6 +146,8 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{ wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: selectedUpstream.SkipCertValidations, SkipTLSValidation: selectedUpstream.SkipCertValidations,
SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck, SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck,
CopyAllHeaders: true,
UserDefinedHeaders: target.HeaderRewriteRules.UserDefinedHeaders,
Logger: h.Parent.Option.Logger, Logger: h.Parent.Option.Logger,
}) })
wspHandler.ServeHTTP(w, r) wspHandler.ServeHTTP(w, r)
@ -160,15 +163,15 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
} }
//Populate the user-defined headers with the values from the request //Populate the user-defined headers with the values from the request
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, target.UserDefinedHeaders) rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, target.HeaderRewriteRules.UserDefinedHeaders)
//Build downstream and upstream header rules //Build downstream and upstream header rules
upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{ upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
UserDefinedHeaders: rewrittenUserDefinedHeaders, UserDefinedHeaders: rewrittenUserDefinedHeaders,
HSTSMaxAge: target.HSTSMaxAge, HSTSMaxAge: target.HeaderRewriteRules.HSTSMaxAge,
HSTSIncludeSubdomains: target.ContainsWildcardName(true), HSTSIncludeSubdomains: target.ContainsWildcardName(true),
EnablePermissionPolicyHeader: target.EnablePermissionPolicyHeader, EnablePermissionPolicyHeader: target.HeaderRewriteRules.EnablePermissionPolicyHeader,
PermissionPolicy: target.PermissionPolicy, PermissionPolicy: target.HeaderRewriteRules.PermissionPolicy,
}) })
//Handle the request reverse proxy //Handle the request reverse proxy
@ -180,8 +183,8 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
PathPrefix: "", PathPrefix: "",
UpstreamHeaders: upstreamHeaders, UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders, DownstreamHeaders: downstreamHeaders,
HostHeaderOverwrite: target.RequestHostOverwrite, HostHeaderOverwrite: target.HeaderRewriteRules.RequestHostOverwrite,
NoRemoveHopByHop: target.DisableHopByHopHeaderRemoval, NoRemoveHopByHop: target.HeaderRewriteRules.DisableHopByHopHeaderRemoval,
Version: target.parent.Option.HostVersion, Version: target.parent.Option.HostVersion,
}) })
@ -223,6 +226,8 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{ wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: target.SkipCertValidations, SkipTLSValidation: target.SkipCertValidations,
SkipOriginCheck: true, //You should not use websocket via virtual directory. But keep this to true for compatibility SkipOriginCheck: true, //You should not use websocket via virtual directory. But keep this to true for compatibility
CopyAllHeaders: true,
UserDefinedHeaders: target.parent.HeaderRewriteRules.UserDefinedHeaders,
Logger: h.Parent.Option.Logger, Logger: h.Parent.Option.Logger,
}) })
wspHandler.ServeHTTP(w, r) wspHandler.ServeHTTP(w, r)
@ -238,15 +243,15 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
} }
//Populate the user-defined headers with the values from the request //Populate the user-defined headers with the values from the request
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, target.parent.UserDefinedHeaders) rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, target.parent.HeaderRewriteRules.UserDefinedHeaders)
//Build downstream and upstream header rules, use the parent (subdomain) endpoint's headers //Build downstream and upstream header rules, use the parent (subdomain) endpoint's headers
upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{ upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
UserDefinedHeaders: rewrittenUserDefinedHeaders, UserDefinedHeaders: rewrittenUserDefinedHeaders,
HSTSMaxAge: target.parent.HSTSMaxAge, HSTSMaxAge: target.parent.HeaderRewriteRules.HSTSMaxAge,
HSTSIncludeSubdomains: target.parent.ContainsWildcardName(true), HSTSIncludeSubdomains: target.parent.ContainsWildcardName(true),
EnablePermissionPolicyHeader: target.parent.EnablePermissionPolicyHeader, EnablePermissionPolicyHeader: target.parent.HeaderRewriteRules.EnablePermissionPolicyHeader,
PermissionPolicy: target.parent.PermissionPolicy, PermissionPolicy: target.parent.HeaderRewriteRules.PermissionPolicy,
}) })
//Handle the virtual directory reverse proxy request //Handle the virtual directory reverse proxy request
@ -257,7 +262,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
PathPrefix: target.MatchingPath, PathPrefix: target.MatchingPath,
UpstreamHeaders: upstreamHeaders, UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders, DownstreamHeaders: downstreamHeaders,
HostHeaderOverwrite: target.parent.RequestHostOverwrite, HostHeaderOverwrite: target.parent.HeaderRewriteRules.RequestHostOverwrite,
Version: target.parent.parent.Option.HostVersion, Version: target.parent.parent.Option.HostVersion,
}) })

View File

@ -70,9 +70,10 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
// Add Proxy Route to current runtime. Call to PrepareProxyRoute before adding to runtime // Add Proxy Route to current runtime. Call to PrepareProxyRoute before adding to runtime
func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error { func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error {
lookupHostname := strings.ToLower(endpoint.RootOrMatchingDomain)
if len(endpoint.ActiveOrigins) == 0 { if len(endpoint.ActiveOrigins) == 0 {
//There are no active origins. No need to check for ready //There are no active origins. No need to check for ready
router.ProxyEndpoints.Store(endpoint.RootOrMatchingDomain, endpoint) router.ProxyEndpoints.Store(lookupHostname, endpoint)
return nil return nil
} }
if !router.loadBalancer.UpstreamsReady(endpoint.ActiveOrigins) { if !router.loadBalancer.UpstreamsReady(endpoint.ActiveOrigins) {
@ -80,7 +81,7 @@ func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error {
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime") return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
} }
// Push record into running subdomain endpoints // Push record into running subdomain endpoints
router.ProxyEndpoints.Store(endpoint.RootOrMatchingDomain, endpoint) router.ProxyEndpoints.Store(lookupHostname, endpoint)
return nil return nil
} }

View File

@ -7,7 +7,7 @@ import (
"sync" "sync"
"imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/auth/sso" "imuslab.com/zoraxy/mod/auth/sso/authelia"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
@ -19,10 +19,12 @@ import (
"imuslab.com/zoraxy/mod/tlscert" "imuslab.com/zoraxy/mod/tlscert"
) )
type ProxyType int
const ( const (
ProxyType_Root = 0 ProxyTypeRoot ProxyType = iota //Root Proxy, everything not matching will be routed here
ProxyType_Host = 1 ProxyTypeHost //Host Proxy, match by host (domain) name
ProxyType_Vdir = 2 ProxyTypeVdir //Virtual Directory Proxy, match by path prefix
) )
type ProxyHandler struct { type ProxyHandler struct {
@ -31,6 +33,7 @@ type ProxyHandler struct {
/* Router Object Options */ /* Router Object Options */
type RouterOption struct { type RouterOption struct {
/* Basic Settings */
HostUUID string //The UUID of Zoraxy, use for heading mod HostUUID string //The UUID of Zoraxy, use for heading mod
HostVersion string //The version of Zoraxy, use for heading mod HostVersion string //The version of Zoraxy, use for heading mod
Port int //Incoming port Port int //Incoming port
@ -39,6 +42,8 @@ type RouterOption struct {
NoCache bool //Force set Cache-Control: no-store NoCache bool //Force set Cache-Control: no-store
ListenOnPort80 bool //Enable port 80 http listener ListenOnPort80 bool //Enable port 80 http listener
ForceHttpsRedirect bool //Force redirection of http to https endpoint ForceHttpsRedirect bool //Force redirection of http to https endpoint
/* Routing Service Managers */
TlsManager *tlscert.Manager //TLS manager for serving SAN certificates TlsManager *tlscert.Manager //TLS manager for serving SAN certificates
RedirectRuleTable *redirection.RuleTable //Redirection rules handler and table RedirectRuleTable *redirection.RuleTable //Redirection rules handler and table
GeodbStore *geodb.Store //GeoIP resolver GeodbStore *geodb.Store //GeoIP resolver
@ -46,21 +51,25 @@ type RouterOption struct {
StatisticCollector *statistic.Collector //Statistic collector for storing stats on incoming visitors StatisticCollector *statistic.Collector //Statistic collector for storing stats on incoming visitors
WebDirectory string //The static web server directory containing the templates folder WebDirectory string //The static web server directory containing the templates folder
LoadBalancer *loadbalance.RouteManager //Load balancer that handle load balancing of proxy target LoadBalancer *loadbalance.RouteManager //Load balancer that handle load balancing of proxy target
SSOHandler *sso.SSOHandler //SSO handler for handling SSO requests, interception mode only
/* Authentication Providers */
AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
/* Utilities */
Logger *logger.Logger //Logger for reverse proxy requets Logger *logger.Logger //Logger for reverse proxy requets
} }
/* Router Object */ /* Router Object */
type Router struct { type Router struct {
Option *RouterOption Option *RouterOption
ProxyEndpoints *sync.Map ProxyEndpoints *sync.Map //Map of ProxyEndpoint objects, each ProxyEndpoint object is a routing rule that handle incoming requests
Running bool Running bool //If the router is running
Root *ProxyEndpoint Root *ProxyEndpoint //Root proxy endpoint, default site
mux http.Handler mux http.Handler //HTTP handler
server *http.Server server *http.Server //HTTP server
tlsListener net.Listener tlsListener net.Listener //TLS listener, handle SNI routing
loadBalancer *loadbalance.RouteManager //Load balancer routing manager loadBalancer *loadbalance.RouteManager //Load balancer routing manager
routingRules []*RoutingRule routingRules []*RoutingRule //Special routing rules, handle high priority routing like ACME request handling
tlsRedirectStop chan bool //Stop channel for tls redirection server tlsRedirectStop chan bool //Stop channel for tls redirection server
rateLimterStop chan bool //Stop channel for rate limiter rateLimterStop chan bool //Stop channel for rate limiter
@ -99,9 +108,48 @@ type VirtualDirectoryEndpoint struct {
parent *ProxyEndpoint `json:"-"` parent *ProxyEndpoint `json:"-"`
} }
// Rules and settings for header rewriting
type HeaderRewriteRules struct {
UserDefinedHeaders []*rewrite.UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
DisableHopByHopHeaderRemoval bool //Do not remove hop-by-hop headers
}
/*
Authentication Provider
TODO: Move these into a dedicated module
*/
type AuthMethod int
const (
AuthMethodNone AuthMethod = iota //No authentication required
AuthMethodBasic //Basic Auth
AuthMethodAuthelia //Authelia
AuthMethodOauth2 //Oauth2
)
type AuthenticationProvider struct {
AuthMethod AuthMethod //The authentication method to use
/* Basic Auth Settings */
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
BasicAuthGroupIDs []string //Group IDs that are allowed to access this endpoint
/* Authelia Settings */
AutheliaURL string //URL of the Authelia server, leave empty to use global settings e.g. authelia.example.com
UseHTTPS bool //Whether to use HTTPS for the Authelia server
}
// A proxy endpoint record, a general interface for handling inbound routing // A proxy endpoint record, a general interface for handling inbound routing
type ProxyEndpoint struct { type ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def ProxyType ProxyType //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key RootOrMatchingDomain string //Matching domain for host, also act as key
MatchingDomainAlias []string //A list of domains that alias to this rule MatchingDomainAlias []string //A list of domains that alias to this rule
ActiveOrigins []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to ActiveOrigins []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to
@ -117,23 +165,18 @@ type ProxyEndpoint struct {
VirtualDirectories []*VirtualDirectoryEndpoint VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers //Custom Headers
UserDefinedHeaders []*rewrite.UserDefinedHeader //Custom headers to append when proxying requests from this endpoint HeaderRewriteRules *HeaderRewriteRules
RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
DisableHopByHopHeaderRemoval bool //Do not remove hop-by-hop headers
//Authentication //Authentication
RequireBasicAuth bool //Set to true to request basic auth before proxy AuthenticationProvider *AuthenticationProvider
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
UseSSOIntercept bool //Allow SSO to intercept this endpoint and provide authentication via Oauth2 credentials
// Rate Limiting // Rate Limiting
RequireRateLimit bool RequireRateLimit bool
RateLimit int64 // Rate limit in requests per second RateLimit int64 // Rate limit in requests per second
//Uptime Monitor
DisableUptimeMonitor bool //Disable uptime monitor for this endpoint
//Access Control //Access Control
AccessFilterUUID string //Access filter ID AccessFilterUUID string //Access filter ID
@ -158,6 +201,9 @@ const (
DefaultSite_ReverseProxy = 1 DefaultSite_ReverseProxy = 1
DefaultSite_Redirect = 2 DefaultSite_Redirect = 2
DefaultSite_NotFoundPage = 3 DefaultSite_NotFoundPage = 3
DefaultSite_NoResponse = 4
DefaultSite_TeaPot = 418 //I'm a teapot
) )
/* /*

View File

@ -3,10 +3,14 @@ package geodb
import ( import (
_ "embed" _ "embed"
"net/http" "net/http"
"os"
"sync"
"time" "time"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/netutils" "imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/utils"
) )
//go:embed geoipv4.csv //go:embed geoipv4.csv
@ -21,8 +25,8 @@ type Store struct {
geotrie *trie geotrie *trie
geotrieIpv6 *trie geotrieIpv6 *trie
sysdb *database.Database sysdb *database.Database
slowLookupCacheIpv4 map[string]string //Cache for slow lookup slowLookupCacheIpv4 sync.Map //Cache for slow lookup, ip -> cc
slowLookupCacheIpv6 map[string]string //Cache for slow lookup slowLookupCacheIpv6 sync.Map //Cache for slow lookup ipv6, ip -> cc
cacheClearTicker *time.Ticker //Ticker for clearing cache cacheClearTicker *time.Ticker //Ticker for clearing cache
cacheClearTickerStopChan chan bool //Stop channel for cache clear ticker cacheClearTickerStopChan chan bool //Stop channel for cache clear ticker
option *StoreOptions option *StoreOptions
@ -31,6 +35,7 @@ type Store struct {
type StoreOptions struct { type StoreOptions struct {
AllowSlowIpv4LookUp bool AllowSlowIpv4LookUp bool
AllowSlowIpv6Lookup bool AllowSlowIpv6Lookup bool
Logger *logger.Logger
SlowLookupCacheClearInterval time.Duration //Clear slow lookup cache interval SlowLookupCacheClearInterval time.Duration //Clear slow lookup cache interval
} }
@ -40,6 +45,23 @@ type CountryInfo struct {
} }
func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) { func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
//Check if external geoDB data is available
if utils.FileExists("./conf/geodb/geoipv4.csv") {
externalV4Db, err := os.ReadFile("./conf/geodb/geoipv4.csv")
if err == nil {
option.Logger.PrintAndLog("GeoDB", "External GeoDB data found, using external IPv4 GeoIP data", nil)
geoipv4 = externalV4Db
}
}
if utils.FileExists("./conf/geodb/geoipv6.csv") {
externalV6Db, err := os.ReadFile("./conf/geodb/geoipv6.csv")
if err == nil {
option.Logger.PrintAndLog("GeoDB", "External GeoDB data found, using external IPv6 GeoIP data", nil)
geoipv6 = externalV6Db
}
}
parsedGeoData, err := parseCSV(geoipv4) parsedGeoData, err := parseCSV(geoipv4)
if err != nil { if err != nil {
return nil, err return nil, err
@ -61,7 +83,7 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
} }
if option.SlowLookupCacheClearInterval == 0 { if option.SlowLookupCacheClearInterval == 0 {
option.SlowLookupCacheClearInterval = 15 * time.Minute option.SlowLookupCacheClearInterval = 30 * time.Minute
} }
//Create a new store //Create a new store
@ -71,8 +93,8 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
geodbIpv6: parsedGeoDataIpv6, geodbIpv6: parsedGeoDataIpv6,
geotrieIpv6: ipv6Trie, geotrieIpv6: ipv6Trie,
sysdb: sysdb, sysdb: sysdb,
slowLookupCacheIpv4: make(map[string]string), slowLookupCacheIpv4: sync.Map{},
slowLookupCacheIpv6: make(map[string]string), slowLookupCacheIpv6: sync.Map{},
cacheClearTicker: time.NewTicker(option.SlowLookupCacheClearInterval), cacheClearTicker: time.NewTicker(option.SlowLookupCacheClearInterval),
cacheClearTickerStopChan: make(chan bool), cacheClearTickerStopChan: make(chan bool),
option: option, option: option,
@ -86,8 +108,8 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
case <-store.cacheClearTickerStopChan: case <-store.cacheClearTickerStopChan:
return return
case <-thisGeoDBStore.cacheClearTicker.C: case <-thisGeoDBStore.cacheClearTicker.C:
thisGeoDBStore.slowLookupCacheIpv4 = make(map[string]string) thisGeoDBStore.slowLookupCacheIpv4 = sync.Map{}
thisGeoDBStore.slowLookupCacheIpv6 = make(map[string]string) thisGeoDBStore.slowLookupCacheIpv6 = sync.Map{}
} }
} }
}(thisGeoDBStore) }(thisGeoDBStore)

View File

@ -4,6 +4,7 @@ import (
"testing" "testing"
"imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
) )
/* /*
@ -42,8 +43,9 @@ func TestTrieConstruct(t *testing.T) {
func TestResolveCountryCodeFromIP(t *testing.T) { func TestResolveCountryCodeFromIP(t *testing.T) {
// Create a new store // Create a new store
store, err := geodb.NewGeoDb(nil, &geodb.StoreOptions{ store, err := geodb.NewGeoDb(nil, &geodb.StoreOptions{
false,
true, true,
true,
&logger.Logger{},
0, 0,
}) })
if err != nil { if err != nil {
@ -84,4 +86,24 @@ func TestResolveCountryCodeFromIP(t *testing.T) {
if info.CountryIsoCode != expected { if info.CountryIsoCode != expected {
t.Errorf("expected country code %s, but got %s for IP %s", expected, info.CountryIsoCode, ip) t.Errorf("expected country code %s, but got %s for IP %s", expected, info.CountryIsoCode, ip)
} }
// Test for issue #401
// Create 100 concurrent goroutines to resolve country code for random IP addresses in the test cases above
for i := 0; i < 100; i++ {
go func() {
for _, testcase := range knownIpCountryMap {
ip := testcase[0]
expected := testcase[1]
info, err := store.ResolveCountryCodeFromIP(ip)
if err != nil {
t.Errorf("error resolving country code for IP %s: %v", ip, err)
return
}
if info.CountryIsoCode != expected {
t.Errorf("expected country code %s, but got %s for IP %s", expected, info.CountryIsoCode, ip)
}
}
}()
}
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -58,7 +58,8 @@ func (s *Store) slowSearchIpv4(ipAddr string) string {
} }
//Check if already in cache //Check if already in cache
if cc, ok := s.slowLookupCacheIpv4[ipAddr]; ok { cc := s.GetSlowSearchCachedIpv4(ipAddr)
if cc != "" {
return cc return cc
} }
@ -70,7 +71,7 @@ func (s *Store) slowSearchIpv4(ipAddr string) string {
inRange, _ := isIPv4InRange(startIp, endIp, ipAddr) inRange, _ := isIPv4InRange(startIp, endIp, ipAddr)
if inRange { if inRange {
//Add to cache //Add to cache
s.slowLookupCacheIpv4[ipAddr] = cc s.slowLookupCacheIpv4.Store(ipAddr, cc)
return cc return cc
} }
} }
@ -83,7 +84,8 @@ func (s *Store) slowSearchIpv6(ipAddr string) string {
} }
//Check if already in cache //Check if already in cache
if cc, ok := s.slowLookupCacheIpv6[ipAddr]; ok { cc := s.GetSlowSearchCachedIpv6(ipAddr)
if cc != "" {
return cc return cc
} }
@ -95,9 +97,27 @@ func (s *Store) slowSearchIpv6(ipAddr string) string {
inRange, _ := isIPv6InRange(startIp, endIp, ipAddr) inRange, _ := isIPv6InRange(startIp, endIp, ipAddr)
if inRange { if inRange {
//Add to cache //Add to cache
s.slowLookupCacheIpv6[ipAddr] = cc s.slowLookupCacheIpv6.Store(ipAddr, cc)
return cc return cc
} }
} }
return "" return ""
} }
// GetSlowSearchCachedIpv4 return the country code for the given ipv4 address, return empty string if not found
func (s *Store) GetSlowSearchCachedIpv4(ipAddr string) string {
cc, ok := s.slowLookupCacheIpv4.Load(ipAddr)
if ok {
return cc.(string)
}
return ""
}
// GetSlowSearchCachedIpv6 return the country code for the given ipv6 address, return empty string if not found
func (s *Store) GetSlowSearchCachedIpv6(ipAddr string) string {
cc, ok := s.slowLookupCacheIpv6.Load(ipAddr)
if ok {
return cc.(string)
}
return ""
}

56
src/mod/geodb/updater.go Normal file
View File

@ -0,0 +1,56 @@
package geodb
import (
"io"
"log"
"net/http"
"os"
"imuslab.com/zoraxy/mod/utils"
)
const (
ipv4UpdateSource = "https://cdn.jsdelivr.net/npm/@ip-location-db/geo-whois-asn-country/geo-whois-asn-country-ipv4.csv"
ipv6UpdateSource = "https://cdn.jsdelivr.net/npm/@ip-location-db/geo-whois-asn-country/geo-whois-asn-country-ipv6.csv"
)
// DownloadGeoDBUpdate download the latest geodb update
func DownloadGeoDBUpdate(externalGeoDBStoragePath string) {
//Create the storage path if not exist
if !utils.FileExists(externalGeoDBStoragePath) {
os.MkdirAll(externalGeoDBStoragePath, 0755)
}
//Download the update
log.Println("Downloading IPv4 database update...")
err := downloadFile(ipv4UpdateSource, externalGeoDBStoragePath+"/geoipv4.csv")
if err != nil {
log.Println(err)
return
}
log.Println("Downloading IPv6 database update...")
err = downloadFile(ipv6UpdateSource, externalGeoDBStoragePath+"/geoipv6.csv")
if err != nil {
log.Println(err)
return
}
log.Println("GeoDB update stored at: " + externalGeoDBStoragePath)
log.Println("Exiting...")
}
// Utility functions
func downloadFile(url string, savepath string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
fileContent, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
return os.WriteFile(savepath, fileContent, 0644)
}

View File

@ -33,15 +33,15 @@ type DailySummary struct {
} }
type RequestInfo struct { type RequestInfo struct {
IpAddr string IpAddr string //IP address of the downstream request
RequestOriginalCountryISOCode string RequestOriginalCountryISOCode string //ISO code of the country where the request originated
Succ bool Succ bool //If the request is successful and resp generated by upstream instead of Zoraxy (except static web server)
StatusCode int StatusCode int //HTTP status code of the request
ForwardType string ForwardType string //Forward type of the request, usually the proxy type (e.g. host-http, subdomain-websocket or vdir-http or any of the combination)
Referer string Referer string //Referer of the downstream request
UserAgent string UserAgent string //UserAgent of the downstream request
RequestURL string RequestURL string //Request URL
Target string Target string //Target domain or hostname
} }
type CollectorOption struct { type CollectorOption struct {
@ -59,7 +59,7 @@ func NewStatisticCollector(option CollectorOption) (*Collector, error) {
//Create the collector object //Create the collector object
thisCollector := Collector{ thisCollector := Collector{
DailySummary: newDailySummary(), DailySummary: NewDailySummary(),
Option: &option, Option: &option,
} }
@ -87,6 +87,11 @@ func (c *Collector) SaveSummaryOfDay() {
c.Option.Database.Write("stats", summaryKey, saveData) c.Option.Database.Write("stats", summaryKey, saveData)
} }
// Get the daily summary up until now
func (c *Collector) GetCurrentDailySummary() *DailySummary {
return c.DailySummary
}
// Load the summary of a day given // Load the summary of a day given
func (c *Collector) LoadSummaryOfDay(year int, month time.Month, day int) *DailySummary { func (c *Collector) LoadSummaryOfDay(year int, month time.Month, day int) *DailySummary {
date := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local) date := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local)
@ -99,7 +104,7 @@ func (c *Collector) LoadSummaryOfDay(year int, month time.Month, day int) *Daily
// Reset today summary, for debug or restoring injections // Reset today summary, for debug or restoring injections
func (c *Collector) ResetSummaryOfDay() { func (c *Collector) ResetSummaryOfDay() {
c.DailySummary = newDailySummary() c.DailySummary = NewDailySummary()
} }
// This function gives the current slot in the 288- 5 minutes interval of the day // This function gives the current slot in the 288- 5 minutes interval of the day
@ -185,8 +190,6 @@ func (c *Collector) RecordRequest(ri RequestInfo) {
c.DailySummary.UserAgent.Store(ri.UserAgent, ua.(int)+1) c.DailySummary.UserAgent.Store(ri.UserAgent, ua.(int)+1)
} }
//ADD MORE HERE IF NEEDED
//Record request URL, if it is a page //Record request URL, if it is a page
ext := filepath.Ext(ri.RequestURL) ext := filepath.Ext(ri.RequestURL)
@ -201,6 +204,8 @@ func (c *Collector) RecordRequest(ri RequestInfo) {
c.DailySummary.RequestURL.Store(ri.RequestURL, ru.(int)+1) c.DailySummary.RequestURL.Store(ri.RequestURL, ru.(int)+1)
} }
}() }()
//ADD MORE HERE IF NEEDED
} }
// nightly task // nightly task
@ -223,7 +228,7 @@ func (c *Collector) ScheduleResetRealtimeStats() chan bool {
case <-time.After(duration): case <-time.After(duration):
// store daily summary to database and reset summary // store daily summary to database and reset summary
c.SaveSummaryOfDay() c.SaveSummaryOfDay()
c.DailySummary = newDailySummary() c.DailySummary = NewDailySummary()
case <-doneCh: case <-doneCh:
// stop the routine // stop the routine
return return
@ -234,7 +239,7 @@ func (c *Collector) ScheduleResetRealtimeStats() chan bool {
return doneCh return doneCh
} }
func newDailySummary() *DailySummary { func NewDailySummary() *DailySummary {
return &DailySummary{ return &DailySummary{
TotalRequest: 0, TotalRequest: 0,
ErrorRequest: 0, ErrorRequest: 0,
@ -247,3 +252,30 @@ func newDailySummary() *DailySummary {
RequestURL: &sync.Map{}, RequestURL: &sync.Map{},
} }
} }
func PrintDailySummary(summary *DailySummary) {
summary.ForwardTypes.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
summary.RequestOrigin.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
summary.RequestClientIp.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
summary.Referer.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
summary.UserAgent.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
summary.RequestURL.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
}

View File

@ -0,0 +1,215 @@
package statistic_test
import (
"net"
"os"
"testing"
"time"
"math/rand"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/database/dbinc"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/statistic"
)
const test_db_path = "test_db"
func getNewDatabase() *database.Database {
db, err := database.NewDatabase(test_db_path, dbinc.BackendLevelDB)
if err != nil {
panic(err)
}
db.NewTable("stats")
return db
}
func clearDatabase(db *database.Database) {
db.Close()
os.RemoveAll(test_db_path)
}
func TestNewStatisticCollector(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, err := statistic.NewStatisticCollector(option)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if collector == nil {
t.Fatalf("Expected collector, got nil")
}
}
func TestSaveSummaryOfDay(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
collector.SaveSummaryOfDay()
// Add assertions to check if data is saved correctly
}
func TestLoadSummaryOfDay(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
year, month, day := time.Now().Date()
summary := collector.LoadSummaryOfDay(year, month, day)
if summary == nil {
t.Fatalf("Expected summary, got nil")
}
}
func TestResetSummaryOfDay(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
collector.ResetSummaryOfDay()
if collector.DailySummary.TotalRequest != 0 {
t.Fatalf("Expected TotalRequest to be 0, got %v", collector.DailySummary.TotalRequest)
}
}
func TestGetCurrentRealtimeStatIntervalId(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
intervalId := collector.GetCurrentRealtimeStatIntervalId()
if intervalId < 0 || intervalId > 287 {
t.Fatalf("Expected intervalId to be between 0 and 287, got %v", intervalId)
}
}
func TestRecordRequest(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
requestInfo := statistic.RequestInfo{
IpAddr: "127.0.0.1",
RequestOriginalCountryISOCode: "US",
Succ: true,
StatusCode: 200,
ForwardType: "type1",
Referer: "http://example.com",
UserAgent: "Mozilla/5.0",
RequestURL: "/test",
Target: "target1",
}
collector.RecordRequest(requestInfo)
time.Sleep(1 * time.Second) // Wait for the goroutine to finish
if collector.DailySummary.TotalRequest != 1 {
t.Fatalf("Expected TotalRequest to be 1, got %v", collector.DailySummary.TotalRequest)
}
}
func TestScheduleResetRealtimeStats(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
stopChan := collector.ScheduleResetRealtimeStats()
if stopChan == nil {
t.Fatalf("Expected stopChan, got nil")
}
collector.Close()
}
func TestNewDailySummary(t *testing.T) {
summary := statistic.NewDailySummary()
if summary.TotalRequest != 0 {
t.Fatalf("Expected TotalRequest to be 0, got %v", summary.TotalRequest)
}
if summary.ForwardTypes == nil {
t.Fatalf("Expected ForwardTypes to be initialized, got nil")
}
if summary.RequestOrigin == nil {
t.Fatalf("Expected RequestOrigin to be initialized, got nil")
}
if summary.RequestClientIp == nil {
t.Fatalf("Expected RequestClientIp to be initialized, got nil")
}
if summary.Referer == nil {
t.Fatalf("Expected Referer to be initialized, got nil")
}
if summary.UserAgent == nil {
t.Fatalf("Expected UserAgent to be initialized, got nil")
}
if summary.RequestURL == nil {
t.Fatalf("Expected RequestURL to be initialized, got nil")
}
}
func generateTestRequestInfo(db *database.Database) statistic.RequestInfo {
//Generate a random IPv4 address
randomIpAddr := ""
for {
ip := net.IPv4(byte(rand.Intn(256)), byte(rand.Intn(256)), byte(rand.Intn(256)), byte(rand.Intn(256)))
if !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsMulticast() && !ip.IsUnspecified() {
randomIpAddr = ip.String()
break
}
}
//Resolve the country code for this IP
ipLocation := "unknown"
geoIpResolver, err := geodb.NewGeoDb(db, &geodb.StoreOptions{
AllowSlowIpv4LookUp: false,
AllowSlowIpv6Lookup: true, //Just to save some RAM
})
if err == nil {
ipInfo, _ := geoIpResolver.ResolveCountryCodeFromIP(randomIpAddr)
ipLocation = ipInfo.CountryIsoCode
}
forwardType := "host-http"
//Generate a random forward type between "subdomain-http" and "host-https"
if rand.Intn(2) == 1 {
forwardType = "subdomain-http"
}
//Generate 5 random refers URL and pick from there
referers := []string{"https://example.com", "https://example.org", "https://example.net", "https://example.io", "https://example.co"}
referer := referers[rand.Intn(5)]
return statistic.RequestInfo{
IpAddr: randomIpAddr,
RequestOriginalCountryISOCode: ipLocation,
Succ: true,
StatusCode: 200,
ForwardType: forwardType,
Referer: referer,
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
RequestURL: "/benchmark",
Target: "test.imuslab.internal",
}
}
func BenchmarkRecordRequest(b *testing.B) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
var requestInfo statistic.RequestInfo = generateTestRequestInfo(db)
b.ResetTimer()
for i := 0; i < b.N; i++ {
collector.RecordRequest(requestInfo)
collector.SaveSummaryOfDay()
}
//Write the current in-memory summary to database file
b.StopTimer()
//Print the generated summary
//testSummary := collector.GetCurrentDailySummary()
//statistic.PrintDailySummary(testSummary)
}

View File

@ -1,6 +1,9 @@
package update package update
import v308 "imuslab.com/zoraxy/mod/update/v308" import (
v308 "imuslab.com/zoraxy/mod/update/v308"
v315 "imuslab.com/zoraxy/mod/update/v315"
)
// Updater Core logic // Updater Core logic
func runUpdateRoutineWithVersion(fromVersion int, toVersion int) { func runUpdateRoutineWithVersion(fromVersion int, toVersion int) {
@ -10,6 +13,12 @@ func runUpdateRoutineWithVersion(fromVersion int, toVersion int) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
} else if fromVersion == 314 && toVersion == 315 {
//Updating from v3.1.4 to v3.1.5
err := v315.UpdateFrom314To315()
if err != nil {
panic(err)
}
} }
//ADD MORE VERSIONS HERE //ADD MORE VERSIONS HERE

View File

@ -0,0 +1,24 @@
package updateutil
import (
"io"
"os"
)
// Helper function to copy files
func CopyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destinationFile, err := os.Create(dst)
if err != nil {
return err
}
defer destinationFile.Close()
_, err = io.Copy(destinationFile, sourceFile)
return err
}

View File

@ -0,0 +1,50 @@
package v315
import (
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
)
// A proxy endpoint record, a general interface for handling inbound routing
type v314ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
MatchingDomainAlias []string //A list of domains that alias to this rule
ActiveOrigins []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to
InactiveOrigins []*loadbalance.Upstream //Disabled Upstream or origin servers IP or domain to proxy to
UseStickySession bool //Use stick session for load balancing
UseActiveLoadBalance bool //Use active loadbalancing, default passive
Disabled bool //If the rule is disabled
//Inbound TLS/SSL Related
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
//Virtual Directories
VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers
UserDefinedHeaders []*rewrite.UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
DisableHopByHopHeaderRemoval bool //Do not remove hop-by-hop headers
//Authentication
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
UseSSOIntercept bool //Allow SSO to intercept this endpoint and provide authentication via Oauth2 credentials
// Rate Limiting
RequireRateLimit bool
RateLimit int64 // Rate limit in requests per second
//Access Control
AccessFilterUUID string //Access filter ID
//Fallback routing logic (Special Rule Sets Only)
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
}

View File

@ -0,0 +1,106 @@
package v315
import (
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
)
type ProxyType int
const (
ProxyTypeRoot ProxyType = iota //Root Proxy, everything not matching will be routed here
ProxyTypeHost //Host Proxy, match by host (domain) name
ProxyTypeVdir //Virtual Directory Proxy, match by path prefix
)
/* Basic Auth Related Data structure*/
// Auth credential for basic auth on certain endpoints
type BasicAuthCredentials struct {
Username string
PasswordHash string
}
// Auth credential for basic auth on certain endpoints
type BasicAuthUnhashedCredentials struct {
Username string
Password string
}
// Paths to exclude in basic auth enabled proxy handler
type BasicAuthExceptionRule struct {
PathPrefix string
}
/* Routing Rule Data Structures */
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
// program structure than directly using ProxyEndpoint
type VirtualDirectoryEndpoint struct {
MatchingPath string //Matching prefix of the request path, also act as key
Domain string //Domain or IP to proxy to
RequireTLS bool //Target domain require TLS
SkipCertValidations bool //Set to true to accept self signed certs
Disabled bool //If the rule is enabled
}
// Rules and settings for header rewriting
type HeaderRewriteRules struct {
UserDefinedHeaders []*rewrite.UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
DisableHopByHopHeaderRemoval bool //Do not remove hop-by-hop headers
}
type AuthProvider int
const (
AuthProviderNone AuthProvider = iota
AuthProviderBasicAuth
AuthProviderAuthelia
AuthProviderOauth2
)
type AuthenticationProvider struct {
AuthProvider AuthProvider //The type of authentication provider
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
}
// A proxy endpoint record, a general interface for handling inbound routing
type v315ProxyEndpoint struct {
ProxyType ProxyType //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
MatchingDomainAlias []string //A list of domains that alias to this rule
ActiveOrigins []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to
InactiveOrigins []*loadbalance.Upstream //Disabled Upstream or origin servers IP or domain to proxy to
UseStickySession bool //Use stick session for load balancing
UseActiveLoadBalance bool //Use active loadbalancing, default passive
Disabled bool //If the rule is disabled
//Inbound TLS/SSL Related
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
//Virtual Directories
VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers
HeaderRewriteRules *HeaderRewriteRules
//Authentication
AuthenticationProvider *AuthenticationProvider
// Rate Limiting
RequireRateLimit bool
RateLimit int64 // Rate limit in requests per second
//Access Control
AccessFilterUUID string //Access filter ID
//Fallback routing logic (Special Rule Sets Only)
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
}

124
src/mod/update/v315/v315.go Normal file
View File

@ -0,0 +1,124 @@
package v315
import (
"encoding/json"
"log"
"os"
"path/filepath"
"imuslab.com/zoraxy/mod/update/updateutil"
)
func UpdateFrom314To315() error {
//Load the configs
oldConfigFiles, err := filepath.Glob("./conf/proxy/*.config")
if err != nil {
return err
}
//Backup all the files
err = os.MkdirAll("./conf/proxy-314.old/", 0775)
if err != nil {
return err
}
for _, oldConfigFile := range oldConfigFiles {
// Extract the file name from the path
fileName := filepath.Base(oldConfigFile)
// Construct the backup file path
backupFile := filepath.Join("./conf/proxy-314.old/", fileName)
// Copy the file to the backup directory
err := updateutil.CopyFile(oldConfigFile, backupFile)
if err != nil {
return err
}
}
//read the config into the old struct
for _, oldConfigFile := range oldConfigFiles {
configContent, err := os.ReadFile(oldConfigFile)
if err != nil {
log.Println("Unable to read config file "+filepath.Base(oldConfigFile), err.Error())
continue
}
thisOldConfigStruct := v314ProxyEndpoint{}
err = json.Unmarshal(configContent, &thisOldConfigStruct)
if err != nil {
log.Println("Unable to parse file "+filepath.Base(oldConfigFile), err.Error())
continue
}
//Convert the old struct to the new struct
thisNewConfigStruct := convertV314ToV315(thisOldConfigStruct)
//Write the new config to file
newConfigContent, err := json.MarshalIndent(thisNewConfigStruct, "", " ")
if err != nil {
log.Println("Unable to marshal new config "+filepath.Base(oldConfigFile), err.Error())
continue
}
err = os.WriteFile(oldConfigFile, newConfigContent, 0664)
if err != nil {
log.Println("Unable to write new config "+filepath.Base(oldConfigFile), err.Error())
continue
}
}
return nil
}
func convertV314ToV315(thisOldConfigStruct v314ProxyEndpoint) v315ProxyEndpoint {
//Move old header and auth configs into struct
newHeaderRewriteRules := HeaderRewriteRules{
UserDefinedHeaders: thisOldConfigStruct.UserDefinedHeaders,
RequestHostOverwrite: thisOldConfigStruct.RequestHostOverwrite,
HSTSMaxAge: thisOldConfigStruct.HSTSMaxAge,
EnablePermissionPolicyHeader: thisOldConfigStruct.EnablePermissionPolicyHeader,
PermissionPolicy: thisOldConfigStruct.PermissionPolicy,
DisableHopByHopHeaderRemoval: thisOldConfigStruct.DisableHopByHopHeaderRemoval,
}
newAuthenticationProvider := AuthenticationProvider{
RequireBasicAuth: thisOldConfigStruct.RequireBasicAuth,
BasicAuthCredentials: thisOldConfigStruct.BasicAuthCredentials,
BasicAuthExceptionRules: thisOldConfigStruct.BasicAuthExceptionRules,
}
//Convert proxy type int to enum
var newConfigProxyType ProxyType
if thisOldConfigStruct.ProxyType == 0 {
newConfigProxyType = ProxyTypeRoot
} else if thisOldConfigStruct.ProxyType == 1 {
newConfigProxyType = ProxyTypeHost
} else if thisOldConfigStruct.ProxyType == 2 {
newConfigProxyType = ProxyTypeVdir
}
//Update the config struct
thisNewConfigStruct := v315ProxyEndpoint{
ProxyType: newConfigProxyType,
RootOrMatchingDomain: thisOldConfigStruct.RootOrMatchingDomain,
MatchingDomainAlias: thisOldConfigStruct.MatchingDomainAlias,
ActiveOrigins: thisOldConfigStruct.ActiveOrigins,
InactiveOrigins: thisOldConfigStruct.InactiveOrigins,
UseStickySession: thisOldConfigStruct.UseStickySession,
UseActiveLoadBalance: thisOldConfigStruct.UseActiveLoadBalance,
Disabled: thisOldConfigStruct.Disabled,
BypassGlobalTLS: thisOldConfigStruct.BypassGlobalTLS,
VirtualDirectories: thisOldConfigStruct.VirtualDirectories,
RequireRateLimit: thisOldConfigStruct.RequireRateLimit,
RateLimit: thisOldConfigStruct.RateLimit,
AccessFilterUUID: thisOldConfigStruct.AccessFilterUUID,
DefaultSiteOption: thisOldConfigStruct.DefaultSiteOption,
DefaultSiteValue: thisOldConfigStruct.DefaultSiteValue,
//Append the new struct into the new config
HeaderRewriteRules: &newHeaderRewriteRules,
AuthenticationProvider: &newAuthenticationProvider,
}
return thisNewConfigStruct
}

View File

@ -83,7 +83,11 @@ func (ws *WebServer) SetEnableDirectoryListing(w http.ResponseWriter, r *http.Re
utils.SendErrorResponse(w, "invalid setting given") utils.SendErrorResponse(w, "invalid setting given")
return return
} }
err = ws.option.Sysdb.Write("webserv", "dirlist", enableList)
if err != nil {
utils.SendErrorResponse(w, "unable to save setting")
return
}
ws.option.EnableDirectoryListing = enableList ws.option.EnableDirectoryListing = enableList
utils.SendOK(w) utils.SendOK(w)
} }

View File

@ -13,6 +13,7 @@ import (
"strings" "strings"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/info/logger" "imuslab.com/zoraxy/mod/info/logger"
) )
@ -58,6 +59,8 @@ type WebsocketProxy struct {
type Options struct { type Options struct {
SkipTLSValidation bool //Skip backend TLS validation SkipTLSValidation bool //Skip backend TLS validation
SkipOriginCheck bool //Skip origin check SkipOriginCheck bool //Skip origin check
CopyAllHeaders bool //Copy all headers from incoming request to backend request
UserDefinedHeaders []*rewrite.UserDefinedHeader //User defined headers
Logger *logger.Logger //Logger, can be nil Logger *logger.Logger //Logger, can be nil
} }
@ -78,7 +81,14 @@ func NewProxy(target *url.URL, options Options) *WebsocketProxy {
u.RawQuery = r.URL.RawQuery u.RawQuery = r.URL.RawQuery
return &u return &u
} }
return &WebsocketProxy{Backend: backend, Verbal: false, Options: options}
// Create a new websocket proxy
wsprox := &WebsocketProxy{Backend: backend, Verbal: false, Options: options}
if options.CopyAllHeaders {
wsprox.Director = DefaultDirector
}
return wsprox
} }
// Utilities function for log printing // Utilities function for log printing
@ -90,6 +100,35 @@ func (w *WebsocketProxy) Println(messsage string, err error) {
log.Println("[websocketproxy] [system:info]"+messsage, err) log.Println("[websocketproxy] [system:info]"+messsage, err)
} }
// DefaultDirector is the default implementation of Director, which copies
// all headers from the incoming request to the outgoing request.
func DefaultDirector(r *http.Request, h http.Header) {
//Copy all header values from request to target header
for k, vv := range r.Header {
for _, v := range vv {
h.Set(k, v)
}
}
// Remove hop-by-hop headers
for _, removePendingHeader := range []string{
"Connection",
"Keep-Alive",
"Proxy-Authenticate",
"Proxy-Authorization",
"Te",
"Trailers",
"Transfer-Encoding",
"Sec-WebSocket-Extensions",
"Sec-WebSocket-Key",
"Sec-WebSocket-Protocol",
"Sec-WebSocket-Version",
"Upgrade",
} {
h.Del(removePendingHeader)
}
}
// ServeHTTP implements the http.Handler that proxies WebSocket connections. // ServeHTTP implements the http.Handler that proxies WebSocket connections.
func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if w.Backend == nil { if w.Backend == nil {
@ -162,6 +201,15 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
w.Director(req, requestHeader) w.Director(req, requestHeader)
} }
// Replace header variables and copy user-defined headers
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(req, w.Options.UserDefinedHeaders)
upstreamHeaders, _ := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
UserDefinedHeaders: rewrittenUserDefinedHeaders,
})
for _, headerValuePair := range upstreamHeaders {
requestHeader.Set(headerValuePair[0], headerValuePair[1])
}
// Connect to the backend URL, also pass the headers we get from the requst // Connect to the backend URL, also pass the headers we get from the requst
// together with the Forwarded headers we prepared above. // together with the Forwarded headers we prepared above.
// TODO: support multiplexing on the same backend connection instead of // TODO: support multiplexing on the same backend connection instead of

View File

@ -98,8 +98,8 @@ func ReverseProxtInit() {
StatisticCollector: statisticCollector, StatisticCollector: statisticCollector,
WebDirectory: *staticWebServerRoot, WebDirectory: *staticWebServerRoot,
AccessController: accessController, AccessController: accessController,
AutheliaRouter: autheliaRouter,
LoadBalancer: loadBalancer, LoadBalancer: loadBalancer,
SSOHandler: ssoHandler,
Logger: SystemWideLogger, Logger: SystemWideLogger,
}) })
if err != nil { if err != nil {
@ -309,10 +309,25 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
} }
} }
//Generate a default authenticaion provider
authMethod := dynamicproxy.AuthMethodNone
if requireBasicAuth {
authMethod = dynamicproxy.AuthMethodBasic
}
thisAuthenticationProvider := dynamicproxy.AuthenticationProvider{
AuthMethod: authMethod,
BasicAuthCredentials: basicAuthCredentials,
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
}
thisCustomHeaderRules := dynamicproxy.HeaderRewriteRules{
UserDefinedHeaders: []*rewrite.UserDefinedHeader{},
}
//Generate a proxy endpoint object //Generate a proxy endpoint object
thisProxyEndpoint := dynamicproxy.ProxyEndpoint{ thisProxyEndpoint := dynamicproxy.ProxyEndpoint{
//I/O //I/O
ProxyType: dynamicproxy.ProxyType_Host, ProxyType: dynamicproxy.ProxyTypeHost,
RootOrMatchingDomain: rootOrMatchingDomain, RootOrMatchingDomain: rootOrMatchingDomain,
MatchingDomainAlias: aliasHostnames, MatchingDomainAlias: aliasHostnames,
ActiveOrigins: []*loadbalance.Upstream{ ActiveOrigins: []*loadbalance.Upstream{
@ -333,11 +348,14 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
//VDir //VDir
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{}, VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
//Custom headers //Custom headers
UserDefinedHeaders: []*rewrite.UserDefinedHeader{},
//Auth //Auth
RequireBasicAuth: requireBasicAuth, AuthenticationProvider: &thisAuthenticationProvider,
BasicAuthCredentials: basicAuthCredentials,
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{}, //Header Rewrite
HeaderRewriteRules: &thisCustomHeaderRules,
//Default Site
DefaultSiteOption: 0, DefaultSiteOption: 0,
DefaultSiteValue: "", DefaultSiteValue: "",
// Rate Limit // Rate Limit
@ -379,7 +397,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
//Write the root options to file //Write the root options to file
rootRoutingEndpoint := dynamicproxy.ProxyEndpoint{ rootRoutingEndpoint := dynamicproxy.ProxyEndpoint{
ProxyType: dynamicproxy.ProxyType_Root, ProxyType: dynamicproxy.ProxyTypeRoot,
RootOrMatchingDomain: "/", RootOrMatchingDomain: "/",
ActiveOrigins: []*loadbalance.Upstream{ ActiveOrigins: []*loadbalance.Upstream{
{ {
@ -453,13 +471,17 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
} }
bypassGlobalTLS := (bpgtls == "true") bypassGlobalTLS := (bpgtls == "true")
// Basic Auth // Auth Provider
rba, _ := utils.PostPara(r, "bauth") authProviderTypeStr, _ := utils.PostPara(r, "authprovider")
if rba == "" { if authProviderTypeStr == "" {
rba = "false" authProviderTypeStr = "0"
} }
requireBasicAuth := (rba == "true") authProviderType, err := strconv.Atoi(authProviderTypeStr)
if err != nil {
utils.SendErrorResponse(w, "Invalid auth provider type")
return
}
// Rate Limiting? // Rate Limiting?
rl, _ := utils.PostPara(r, "rate") rl, _ := utils.PostPara(r, "rate")
@ -494,7 +516,23 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
//Generate a new proxyEndpoint from the new config //Generate a new proxyEndpoint from the new config
newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry) newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
newProxyEndpoint.RequireBasicAuth = requireBasicAuth if newProxyEndpoint.AuthenticationProvider == nil {
newProxyEndpoint.AuthenticationProvider = &dynamicproxy.AuthenticationProvider{
AuthMethod: dynamicproxy.AuthMethodNone,
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
}
}
if authProviderType == 1 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodBasic
} else if authProviderType == 2 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthelia
} else if authProviderType == 3 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodOauth2
} else {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodNone
}
newProxyEndpoint.RequireRateLimit = requireRateLimit newProxyEndpoint.RequireRateLimit = requireRateLimit
newProxyEndpoint.RateLimit = proxyRateLimit newProxyEndpoint.RateLimit = proxyRateLimit
newProxyEndpoint.UseStickySession = useStickySession newProxyEndpoint.UseStickySession = useStickySession
@ -624,7 +662,7 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
} }
usernames := []string{} usernames := []string{}
for _, cred := range targetProxy.BasicAuthCredentials { for _, cred := range targetProxy.AuthenticationProvider.BasicAuthCredentials {
usernames = append(usernames, cred.Username) usernames = append(usernames, cred.Username)
} }
@ -668,7 +706,7 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
if credential.Password == "" { if credential.Password == "" {
//Check if exists in the old credential files //Check if exists in the old credential files
keepUnchange := false keepUnchange := false
for _, oldCredEntry := range targetProxy.BasicAuthCredentials { for _, oldCredEntry := range targetProxy.AuthenticationProvider.BasicAuthCredentials {
if oldCredEntry.Username == credential.Username { if oldCredEntry.Username == credential.Username {
//Exists! Reuse the old hash //Exists! Reuse the old hash
mergedCredentials = append(mergedCredentials, &dynamicproxy.BasicAuthCredentials{ mergedCredentials = append(mergedCredentials, &dynamicproxy.BasicAuthCredentials{
@ -693,7 +731,7 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
} }
} }
targetProxy.BasicAuthCredentials = mergedCredentials targetProxy.AuthenticationProvider.BasicAuthCredentials = mergedCredentials
//Save it to file //Save it to file
SaveReverseProxyConfig(targetProxy) SaveReverseProxyConfig(targetProxy)
@ -727,7 +765,7 @@ func ListProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
} }
//List all the exception paths for this proxy //List all the exception paths for this proxy
results := targetProxy.BasicAuthExceptionRules results := targetProxy.AuthenticationProvider.BasicAuthExceptionRules
if results == nil { if results == nil {
//It is a config from a really old version of zoraxy. Overwrite it with empty array //It is a config from a really old version of zoraxy. Overwrite it with empty array
results = []*dynamicproxy.BasicAuthExceptionRule{} results = []*dynamicproxy.BasicAuthExceptionRule{}
@ -764,7 +802,7 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
//Add a new exception rule if it is not already exists //Add a new exception rule if it is not already exists
alreadyExists := false alreadyExists := false
for _, thisExceptionRule := range targetProxy.BasicAuthExceptionRules { for _, thisExceptionRule := range targetProxy.AuthenticationProvider.BasicAuthExceptionRules {
if thisExceptionRule.PathPrefix == matchingPrefix { if thisExceptionRule.PathPrefix == matchingPrefix {
alreadyExists = true alreadyExists = true
break break
@ -774,7 +812,7 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
utils.SendErrorResponse(w, "This matching path already exists") utils.SendErrorResponse(w, "This matching path already exists")
return return
} }
targetProxy.BasicAuthExceptionRules = append(targetProxy.BasicAuthExceptionRules, &dynamicproxy.BasicAuthExceptionRule{ targetProxy.AuthenticationProvider.BasicAuthExceptionRules = append(targetProxy.AuthenticationProvider.BasicAuthExceptionRules, &dynamicproxy.BasicAuthExceptionRule{
PathPrefix: strings.TrimSpace(matchingPrefix), PathPrefix: strings.TrimSpace(matchingPrefix),
}) })
@ -808,7 +846,7 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
newExceptionRuleList := []*dynamicproxy.BasicAuthExceptionRule{} newExceptionRuleList := []*dynamicproxy.BasicAuthExceptionRule{}
matchingExists := false matchingExists := false
for _, thisExceptionalRule := range targetProxy.BasicAuthExceptionRules { for _, thisExceptionalRule := range targetProxy.AuthenticationProvider.BasicAuthExceptionRules {
if thisExceptionalRule.PathPrefix != matchingPrefix { if thisExceptionalRule.PathPrefix != matchingPrefix {
newExceptionRuleList = append(newExceptionRuleList, thisExceptionalRule) newExceptionRuleList = append(newExceptionRuleList, thisExceptionalRule)
} else { } else {
@ -821,7 +859,7 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
return return
} }
targetProxy.BasicAuthExceptionRules = newExceptionRuleList targetProxy.AuthenticationProvider.BasicAuthExceptionRules = newExceptionRuleList
// Save configs to runtime and file // Save configs to runtime and file
targetProxy.UpdateToRuntime() targetProxy.UpdateToRuntime()
@ -885,6 +923,7 @@ func ReverseProxyListDetail(w http.ResponseWriter, r *http.Request) {
utils.SendErrorResponse(w, "epname not defined") utils.SendErrorResponse(w, "epname not defined")
return return
} }
epname = strings.ToLower(strings.TrimSpace(epname))
endpointRaw, ok := dynamicProxyRouter.ProxyEndpoints.Load(epname) endpointRaw, ok := dynamicProxyRouter.ProxyEndpoints.Load(epname)
if !ok { if !ok {
utils.SendErrorResponse(w, "proxy rule not found") utils.SendErrorResponse(w, "proxy rule not found")
@ -914,13 +953,13 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
thisEndpoint := dynamicproxy.CopyEndpoint(value.(*dynamicproxy.ProxyEndpoint)) thisEndpoint := dynamicproxy.CopyEndpoint(value.(*dynamicproxy.ProxyEndpoint))
//Clear the auth passwords before showing to front-end //Clear the auth passwords before showing to front-end
cleanedCredentials := []*dynamicproxy.BasicAuthCredentials{} cleanedCredentials := []*dynamicproxy.BasicAuthCredentials{}
for _, user := range thisEndpoint.BasicAuthCredentials { for _, user := range thisEndpoint.AuthenticationProvider.BasicAuthCredentials {
cleanedCredentials = append(cleanedCredentials, &dynamicproxy.BasicAuthCredentials{ cleanedCredentials = append(cleanedCredentials, &dynamicproxy.BasicAuthCredentials{
Username: user.Username, Username: user.Username,
PasswordHash: "", PasswordHash: "",
}) })
} }
thisEndpoint.BasicAuthCredentials = cleanedCredentials thisEndpoint.AuthenticationProvider.BasicAuthCredentials = cleanedCredentials
results = append(results, thisEndpoint) results = append(results, thisEndpoint)
return true return true
}) })
@ -1127,7 +1166,7 @@ func HandleCustomHeaderList(w http.ResponseWriter, r *http.Request) {
} }
//List all custom headers //List all custom headers
customHeaderList := targetProxyEndpoint.UserDefinedHeaders customHeaderList := targetProxyEndpoint.HeaderRewriteRules.UserDefinedHeaders
if customHeaderList == nil { if customHeaderList == nil {
customHeaderList = []*rewrite.UserDefinedHeader{} customHeaderList = []*rewrite.UserDefinedHeader{}
} }
@ -1269,7 +1308,7 @@ func HandleHostOverwrite(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
//Get the current host header //Get the current host header
js, _ := json.Marshal(targetProxyEndpoint.RequestHostOverwrite) js, _ := json.Marshal(targetProxyEndpoint.HeaderRewriteRules.RequestHostOverwrite)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
} else if r.Method == http.MethodPost { } else if r.Method == http.MethodPost {
//Set the new host header //Set the new host header
@ -1278,7 +1317,7 @@ func HandleHostOverwrite(w http.ResponseWriter, r *http.Request) {
//As this will require change in the proxy instance we are running //As this will require change in the proxy instance we are running
//we need to clone and respawn this proxy endpoint //we need to clone and respawn this proxy endpoint
newProxyEndpoint := targetProxyEndpoint.Clone() newProxyEndpoint := targetProxyEndpoint.Clone()
newProxyEndpoint.RequestHostOverwrite = newHostname newProxyEndpoint.HeaderRewriteRules.RequestHostOverwrite = newHostname
//Save proxy endpoint //Save proxy endpoint
err = SaveReverseProxyConfig(newProxyEndpoint) err = SaveReverseProxyConfig(newProxyEndpoint)
if err != nil { if err != nil {
@ -1341,7 +1380,7 @@ func HandleHopByHop(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
//Get the current hop by hop header state //Get the current hop by hop header state
js, _ := json.Marshal(!targetProxyEndpoint.DisableHopByHopHeaderRemoval) js, _ := json.Marshal(!targetProxyEndpoint.HeaderRewriteRules.DisableHopByHopHeaderRemoval)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
} else if r.Method == http.MethodPost { } else if r.Method == http.MethodPost {
//Set the hop by hop header state //Set the hop by hop header state
@ -1351,7 +1390,7 @@ func HandleHopByHop(w http.ResponseWriter, r *http.Request) {
//we need to clone and respawn this proxy endpoint //we need to clone and respawn this proxy endpoint
newProxyEndpoint := targetProxyEndpoint.Clone() newProxyEndpoint := targetProxyEndpoint.Clone()
//Storage file use false as default, so disable removal = not enable remover //Storage file use false as default, so disable removal = not enable remover
newProxyEndpoint.DisableHopByHopHeaderRemoval = !enableHopByHopRemover newProxyEndpoint.HeaderRewriteRules.DisableHopByHopHeaderRemoval = !enableHopByHopRemover
//Save proxy endpoint //Save proxy endpoint
err = SaveReverseProxyConfig(newProxyEndpoint) err = SaveReverseProxyConfig(newProxyEndpoint)
@ -1414,7 +1453,7 @@ func HandleHSTSState(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
//Return current HSTS enable state //Return current HSTS enable state
hstsAge := targetProxyEndpoint.HSTSMaxAge hstsAge := targetProxyEndpoint.HeaderRewriteRules.HSTSMaxAge
js, _ := json.Marshal(hstsAge) js, _ := json.Marshal(hstsAge)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
return return
@ -1426,8 +1465,12 @@ func HandleHSTSState(w http.ResponseWriter, r *http.Request) {
} }
if newMaxAge == 0 || newMaxAge >= 31536000 { if newMaxAge == 0 || newMaxAge >= 31536000 {
targetProxyEndpoint.HSTSMaxAge = int64(newMaxAge) targetProxyEndpoint.HeaderRewriteRules.HSTSMaxAge = int64(newMaxAge)
SaveReverseProxyConfig(targetProxyEndpoint) err = SaveReverseProxyConfig(targetProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, "save HSTS state failed: "+err.Error())
return
}
targetProxyEndpoint.UpdateToRuntime() targetProxyEndpoint.UpdateToRuntime()
} else { } else {
utils.SendErrorResponse(w, "invalid max age given") utils.SendErrorResponse(w, "invalid max age given")
@ -1464,11 +1507,11 @@ func HandlePermissionPolicy(w http.ResponseWriter, r *http.Request) {
} }
currentPolicy := permissionpolicy.GetDefaultPermissionPolicy() currentPolicy := permissionpolicy.GetDefaultPermissionPolicy()
if targetProxyEndpoint.PermissionPolicy != nil { if targetProxyEndpoint.HeaderRewriteRules.PermissionPolicy != nil {
currentPolicy = targetProxyEndpoint.PermissionPolicy currentPolicy = targetProxyEndpoint.HeaderRewriteRules.PermissionPolicy
} }
result := CurrentPolicyState{ result := CurrentPolicyState{
PPEnabled: targetProxyEndpoint.EnablePermissionPolicyHeader, PPEnabled: targetProxyEndpoint.HeaderRewriteRules.EnablePermissionPolicyHeader,
CurrentPolicy: currentPolicy, CurrentPolicy: currentPolicy,
} }
@ -1483,7 +1526,7 @@ func HandlePermissionPolicy(w http.ResponseWriter, r *http.Request) {
return return
} }
targetProxyEndpoint.EnablePermissionPolicyHeader = enableState targetProxyEndpoint.HeaderRewriteRules.EnablePermissionPolicyHeader = enableState
SaveReverseProxyConfig(targetProxyEndpoint) SaveReverseProxyConfig(targetProxyEndpoint)
targetProxyEndpoint.UpdateToRuntime() targetProxyEndpoint.UpdateToRuntime()
utils.SendOK(w) utils.SendOK(w)
@ -1505,7 +1548,7 @@ func HandlePermissionPolicy(w http.ResponseWriter, r *http.Request) {
} }
//Save it to file //Save it to file
targetProxyEndpoint.PermissionPolicy = newPermissionPolicy targetProxyEndpoint.HeaderRewriteRules.PermissionPolicy = newPermissionPolicy
SaveReverseProxyConfig(targetProxyEndpoint) SaveReverseProxyConfig(targetProxyEndpoint)
targetProxyEndpoint.UpdateToRuntime() targetProxyEndpoint.UpdateToRuntime()
utils.SendOK(w) utils.SendOK(w)

View File

@ -12,7 +12,9 @@ import (
"imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth" "imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/auth/sso/authelia"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/database/dbinc"
"imuslab.com/zoraxy/mod/dockerux" "imuslab.com/zoraxy/mod/dockerux"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection" "imuslab.com/zoraxy/mod/dynamicproxy/redirection"
@ -64,7 +66,14 @@ func startupSequence() {
}) })
//Create database //Create database
db, err := database.NewDatabase(DATABASE_PATH, false) backendType := database.GetRecommendedBackendType()
if *databaseBackend == "leveldb" {
backendType = dbinc.BackendLevelDB
} else if *databaseBackend == "boltdb" {
backendType = dbinc.BackendBoltDB
}
l.PrintAndLog("database", "Using "+backendType.String()+" as the database backend", nil)
db, err := database.NewDatabase(DATABASE_PATH, backendType)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -105,6 +114,7 @@ func startupSequence() {
geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{ geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{
AllowSlowIpv4LookUp: !*enableHighSpeedGeoIPLookup, AllowSlowIpv4LookUp: !*enableHighSpeedGeoIPLookup,
AllowSlowIpv6Lookup: !*enableHighSpeedGeoIPLookup, AllowSlowIpv6Lookup: !*enableHighSpeedGeoIPLookup,
Logger: SystemWideLogger,
SlowLookupCacheClearInterval: GEODB_CACHE_CLEAR_INTERVAL * time.Minute, SlowLookupCacheClearInterval: GEODB_CACHE_CLEAR_INTERVAL * time.Minute,
}) })
if err != nil { if err != nil {
@ -128,21 +138,13 @@ func startupSequence() {
panic(err) panic(err)
} }
/* //Create authentication providers
//Create an SSO handler autheliaRouter = authelia.NewAutheliaRouter(&authelia.AutheliaRouterOptions{
ssoHandler, err = sso.NewSSOHandler(&sso.SSOConfig{ UseHTTPS: false, // Automatic populate in router initiation
SystemUUID: nodeUUID, AutheliaURL: "", // Automatic populate in router initiation
PortalServerPort: 5488,
AuthURL: "http://auth.localhost",
Database: sysdb,
Logger: SystemWideLogger, Logger: SystemWideLogger,
Database: sysdb,
}) })
if err != nil {
log.Fatal(err)
}
//Restore the SSO handler to previous state before shutdown
ssoHandler.RestorePreviousRunningState()
*/
//Create a statistic collector //Create a statistic collector
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{ statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
@ -323,6 +325,7 @@ func startupSequence() {
} }
/* Finalize Startup Sequence */
// This sequence start after everything is initialized // This sequence start after everything is initialized
func finalSequence() { func finalSequence() {
//Start ACME renew agent //Start ACME renew agent
@ -331,3 +334,45 @@ func finalSequence() {
//Inject routing rules //Inject routing rules
registerBuildInRoutingRules() registerBuildInRoutingRules()
} }
/* Shutdown Sequence */
func ShutdownSeq() {
SystemWideLogger.Println("Shutting down " + SYSTEM_NAME)
SystemWideLogger.Println("Closing Netstats Listener")
if netstatBuffers != nil {
netstatBuffers.Close()
}
SystemWideLogger.Println("Closing Statistic Collector")
if statisticCollector != nil {
statisticCollector.Close()
}
if mdnsTickerStop != nil {
SystemWideLogger.Println("Stopping mDNS Discoverer (might take a few minutes)")
// Stop the mdns service
mdnsTickerStop <- true
}
if mdnsScanner != nil {
mdnsScanner.Close()
}
SystemWideLogger.Println("Shutting down load balancer")
if loadBalancer != nil {
loadBalancer.Close()
}
SystemWideLogger.Println("Closing Certificates Auto Renewer")
if acmeAutoRenewer != nil {
acmeAutoRenewer.Close()
}
//Remove the tmp folder
SystemWideLogger.Println("Cleaning up tmp files")
os.RemoveAll("./tmp")
//Close database
SystemWideLogger.Println("Stopping system database")
sysdb.Close()
//Close logger
SystemWideLogger.Println("Closing system wide logger")
SystemWideLogger.Close()
}

View File

@ -125,10 +125,12 @@
</td> </td>
<td data-label="" editable="true" datatype="vdir">${vdList}</td> <td data-label="" editable="true" datatype="vdir">${vdList}</td>
<td data-label="" editable="true" datatype="advanced" style="width: 350px;"> <td data-label="" editable="true" datatype="advanced" style="width: 350px;">
${subd.RequireBasicAuth?`<i class="ui green check icon"></i> Basic Auth`:``} ${subd.AuthenticationProvider.AuthMethod == 0x1?`<i class="ui grey key icon"></i> Basic Auth`:``}
${subd.RequireBasicAuth && subd.RequireRateLimit?"<br>":""} ${subd.AuthenticationProvider.AuthMethod == 0x2?`<i class="ui blue key icon"></i> Authelia`:``}
${subd.AuthenticationProvider.AuthMethod == 0x3?`<i class="ui yellow key icon"></i> Oauth2`:``}
${subd.AuthenticationProvider.AuthMethod != 0x0 && subd.RequireRateLimit?"<br>":""}
${subd.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:``} ${subd.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:``}
${!subd.RequireBasicAuth && !subd.RequireRateLimit?`<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Special Settings</small>`:""} ${subd.AuthenticationProvider.AuthMethod == 0x0 && !subd.RequireRateLimit?`<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Special Settings</small>`:""}
</td> </td>
<td class="center aligned ignoremw" editable="true" datatype="action" data-label=""> <td class="center aligned ignoremw" editable="true" datatype="action" data-label="">
<div class="ui toggle tiny fitted checkbox" style="margin-bottom: -0.5em; margin-right: 0.4em;" title="Enable / Disable Rule"> <div class="ui toggle tiny fitted checkbox" style="margin-bottom: -0.5em; margin-right: 0.4em;" title="Enable / Disable Rule">
@ -194,6 +196,11 @@
} }
let rule = accessRuleMap[thisAccessRuleID]; let rule = accessRuleMap[thisAccessRuleID];
if (rule == undefined){
//Missing config or config too old
$(this).html(`<i class="ui red exclamation triangle icon"></i> <b style="color: #db2828;">Access Rule Error</b>`);
return;
}
let icon = `<i class="ui grey filter icon"></i>`; let icon = `<i class="ui grey filter icon"></i>`;
if (rule.ID == "default"){ if (rule.ID == "default"){
icon = `<i class="ui yellow star icon"></i>`; icon = `<i class="ui yellow star icon"></i>`;
@ -269,11 +276,7 @@
</button>`); </button>`);
}else if (datatype == "advanced"){ }else if (datatype == "advanced"){
let requireBasicAuth = payload.RequireBasicAuth; let authProvider = payload.AuthenticationProvider.AuthMethod;
let basicAuthCheckstate = "";
if (requireBasicAuth){
basicAuthCheckstate = "checked";
}
let skipWebSocketOriginCheck = payload.SkipWebSocketOriginCheck; let skipWebSocketOriginCheck = payload.SkipWebSocketOriginCheck;
let wsCheckstate = ""; let wsCheckstate = "";
@ -296,13 +299,29 @@
rateLimitDisableState = "disabled"; rateLimitDisableState = "disabled";
} }
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;"> column.empty().append(`
<input type="checkbox" class="RequireBasicAuth" ${basicAuthCheckstate}> <div class="grouped fields authProviderPicker">
<label>Require Basic Auth</label> <label><b>Authentication Provider</b></label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="0" name="authProviderType" ${authProvider==0x0?"checked":""}>
<label>None (Anyone can access)</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="1" name="authProviderType" ${authProvider==0x1?"checked":""}>
<label>Basic Auth</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="2" name="authProviderType" ${authProvider==0x2?"checked":""}>
<label>Authelia</label>
</div>
</div>
</div> </div>
<br>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${uuid}');"><i class="ui blue user circle icon"></i> Edit Credentials</button> <button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${uuid}');"><i class="ui blue user circle icon"></i> Edit Credentials</button>
<br>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editCustomHeaders('${uuid}');"><i class="heading icon"></i> Custom Headers</button> <button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editCustomHeaders('${uuid}');"><i class="heading icon"></i> Custom Headers</button>
<div class="ui basic advance segment" style="padding: 0.4em !important; border-radius: 0.4em;"> <div class="ui basic advance segment" style="padding: 0.4em !important; border-radius: 0.4em;">
@ -328,6 +347,7 @@
<div> <div>
`); `);
$('.authProviderPicker .ui.checkbox').checkbox();
} else if (datatype == "ratelimit"){ } else if (datatype == "ratelimit"){
column.empty().append(` column.empty().append(`
@ -421,7 +441,7 @@
var epttype = "host"; var epttype = "host";
let useStickySession = $(row).find(".UseStickySession")[0].checked; let useStickySession = $(row).find(".UseStickySession")[0].checked;
let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked; let authProviderType = $(row).find(".authProviderPicker input[type='radio']:checked").val();
let requireRateLimit = $(row).find(".RequireRateLimit")[0].checked; let requireRateLimit = $(row).find(".RequireRateLimit")[0].checked;
let rateLimit = $(row).find(".RateLimit").val(); let rateLimit = $(row).find(".RateLimit").val();
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked; let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
@ -434,7 +454,7 @@
"rootname": uuid, "rootname": uuid,
"ss":useStickySession, "ss":useStickySession,
"bpgtls": bypassGlobalTLS, "bpgtls": bypassGlobalTLS,
"bauth" :requireBasicAuth, "authprovider" :authProviderType,
"rate" :requireRateLimit, "rate" :requireRateLimit,
"ratenum" :rateLimit, "ratenum" :rateLimit,
}, },

View File

@ -37,6 +37,14 @@
</label> </label>
</div> </div>
</div> </div>
<div class="field">
<div class="ui radio defaultsite checkbox">
<input type="radio" name="defaultsiteOption" value="closeresp">
<label>Close Connection<br>
<small>Close the connection without any response or in TLS mode, send an empty response</small>
</label>
</div>
</div>
</div> </div>
</div> </div>
@ -105,6 +113,8 @@
currentDefaultSiteOption = 2; currentDefaultSiteOption = 2;
}else if (selectedDefaultSite == "notfound"){ }else if (selectedDefaultSite == "notfound"){
currentDefaultSiteOption = 3; currentDefaultSiteOption = 3;
}else if (selectedDefaultSite == "closeresp"){
currentDefaultSiteOption = 4;
}else{ }else{
//Unknown option //Unknown option
return; return;
@ -137,6 +147,8 @@
$("#redirectDomain").val(data.DefaultSiteValue); $("#redirectDomain").val(data.DefaultSiteValue);
}else if (proxyType == 3){ }else if (proxyType == 3){
$radios.filter('[value=notfound]').prop('checked', true); $radios.filter('[value=notfound]').prop('checked', true);
}else if (proxyType == 4){
$radios.filter('[value=closeresp]').prop('checked', true);
} }
updateAvaibleDefaultSiteOptions(); updateAvaibleDefaultSiteOptions();

View File

@ -1,381 +1,79 @@
<div class="standardContainer"> <div class="standardContainer">
<div class="ui basic segment"> <div class="ui basic segment">
<div class="ui message"> <h2>SSO</h2>
<div class="header"> <p>Single Sign-On (SSO) and authentication providers settings </p>
Work in Progress
</div>
<p>The SSO feature is currently under development.</p>
</div>
</div>
</div>
<!--
<div class="standardContainer">
<div class="ui basic segment">
<h2>Zoraxy SSO / Oauth</h2>
<p>A centralized authentication system for all your subdomains</p>
<div class="ui divider"></div>
<div class="ui basic segment enabled ssoRunningState">
<h4 class="ui header" id="ssoRunningState">
<i class="circle check icon"></i>
<div class="content">
<span class="webserv_status">Running</span>
<div class="sub header">Listen port :<span class="oauthserv_port">8081</span></div>
</div>
</h4>
</div>
<div class="ui form">
<h3 class="ui dividing header">Oauth2 Server Settings</h3>
<div class="field">
<div class="ui toggle checkbox">
<input type="checkbox" name="enableOauth2">
<label>Enable Oauth2 Server<br>
<small>Oauth2 server for handling external authentication requests</small></label>
</div>
</div>
<div class="field">
<label>Oauth2 Server Port</label>
<div class="ui action input">
<input type="number" name="oauth2Port" placeholder="Port" value="5488">
<button id="saveOauthServerPortBtn" class="ui basic green button"><i class="ui green circle check icon"></i> Update</button>
</div>
<small>Listening port of the Zoraxy internal Oauth2 Server.You can create a subdomain proxy rule to <code>127.0.0.1:<span class="ssoPort">5488</span></code></small>
</div>
<div class="field">
<label>Auth URL</label>
<div class="ui action input">
<input type="text" name="authURL" placeholder="https://auth.yourdomain.com">
<button id="saveAuthURLBtn" class="ui basic blue button"><i class="ui blue save icon"></i> Save</button>
</div>
<small>The exposed authentication URL of the Oauth2 server, usually <code>https://auth.example.com</code> or <code>https://sso.yourdomain.com</code>. <b>Remember to include the http:// or https:// in your URL.</b></small>
</div>
</div>
<br>
<div class="ui form">
<h3 class="ui dividing header">Zoraxy SSO Settings</h3>
<div class="field">
<label>Default Redirection URL </label>
<div class="ui fluid input">
<input type="text" name="defaultSiteURL" placeholder="https://yourdomain.com">
</div>
<small>The default URL to redirect to after login if redirection target is not set</small>
</div> </div>
<button class="ui basic button"> <i class="ui green check icon"></i> Apply Changes </button> <div class="ui basic segment">
</div> <div class="ui yellow message">
<div class="ui basic message">
<div class="header"> <div class="header">
<i class="ui yellow exclamation triangle icon"></i> Important Notes about Zoraxy SSO Experimental Feature
</div>
<p>Please note that this feature is still in development and may not work as expected.</p>
</div> </div>
<p>Zoraxy SSO, if enabled in HTTP Proxy rule, will automatically intercept the proxy request and provide an SSO interface on upstreams that do not support OAuth natively.
It is basically like basic auth with a login page. <b> The same user credential can be used in OAuth sign-in and Zoraxy SSO sign-in.</b>
</p>
</div> </div>
<div class="ui divider"></div> <div class="ui divider"></div>
<div> <div class="ui basic segment">
<h3 class="ui header"> <h3>Authelia</h3>
<i class="ui blue user circle icon"></i> <p>Configuration settings for Authelia authentication provider.</p>
<div class="content">
Registered Users <form class="ui form">
<div class="sub header">A list of users that are registered with the SSO server</div> <div class="field">
<label for="autheliaServerUrl">Authelia Server URL</label>
<input type="text" id="autheliaServerUrl" name="autheliaServerUrl" placeholder="Enter Authelia Server URL">
<small>Example: auth.example.com</small>
</div> </div>
</h3> <div class="field">
<table class="ui celled table"> <div class="ui checkbox">
<thead> <input type="checkbox" id="useHttps" name="useHttps">
<tr> <label for="useHttps">Use HTTPS</label>
<th>Username</th> <small>Check this if your authelia server uses HTTPS</small>
<th>Registered On</th> </div>
<th>Reset Password</th> </div>
<th>Remove</th> <button class="ui basic button" onclick="event.preventDefault(); updateAutheliaSettings();"><i class="green check icon"></i> Apply Change</button>
</tr> </form>
</thead>
<tbody id="registeredSsoUsers">
<tr>
<td>admin</td>
<td>2020-01-01</td>
<td><button class="ui blue basic small icon button"><i class="ui blue key icon"></i></button></td>
<td><button class="ui red basic small icon button"><i class="ui red trash icon"></i></button></td>
</tr>
</tbody>
</table>
<button onclick="handleUserListRefresh();" class="ui basic right floated button"><i class="ui green refresh icon"></i> Refresh</button>
<button onclick="openRegisteredUserManager();" class="ui basic button"><i class="ui blue users icon"></i> Manage Registered Users</button>
</div> </div>
<div class="ui divider"></div> <div class="ui divider"></div>
<div>
<h3 class="ui header">
<i class="ui green th icon"></i>
<div class="content">
Registered Apps
<div class="sub header">A list of apps that are registered with the SSO server</div>
</div>
</h3>
<table class="ui celled table">
<thead>
<tr>
<th>App Name</th>
<th>Domain</th>
<th>App ID</th>
<th>Registered On</th>
<th>Remove</th>
</tr>
</thead>
<tbody id="registeredSsoApps">
<tr>
<td>My App</td>
<td><a href="//example.com" target="_blank">example.com</a></td>
<td>123456</td>
<td>2020-01-01</td>
<td><button class="ui red basic small icon button"><i class="ui red trash icon"></i></button></td>
</tr>
</tbody>
</table>
<button onclick="handleRegisterAppListRefresh();" class="ui basic right floated button"><i class="ui green refresh icon"></i> Refresh</button>
<button onclick="openRegisterAppManagementSnippet();" class="ui basic button"><i style="font-size: 1em; margin-top: -0.2em;" class="ui green th large icon"></i> Manage Registered App</button>
<p></p>
</div>
</div>
</div> </div>
<script> <script>
$("input[name=oauth2Port]").on("change", function() { $(document).ready(function() {
$(".ssoPort").text($(this).val());
});
function updateSSOStatus(){
$.get("/api/sso/status", function(data){
if(data.error != undefined){
//Show error message
$(".ssoRunningState").removeClass("enabled").addClass("disabled");
$("#ssoRunningState .webserv_status").html('Error: '+data.error);
}else{
if (data.Enabled){
$(".ssoRunningState").addClass("enabled");
$("#ssoRunningState .webserv_status").html('Running');
$(".ssoRunningState i").attr("class", "circle check icon");
$("input[name=enableOauth2]").parent().checkbox("set checked");
}else{
$(".ssoRunningState").removeClass("enabled");
$("#ssoRunningState .webserv_status").html('Stopped');
$(".ssoRunningState i").attr("class", "circle times icon");
$("input[name=enableOauth2]").parent().checkbox("set unchecked");
}
$("input[name=oauth2Port]").val(data.ListeningPort);
$(".oauthserv_port").text(data.ListeningPort);
$("input[name=authURL]").val(data.AuthURL);
}
});
}
function initSSOStatus(){
$.get("/api/sso/status", function(data){
//Update the SSO status from the server
updateSSOStatus();
//Bind events to the enable checkbox
$("input[name=enableOauth2]").off("change").on("change", function(){
var checked = $(this).prop("checked");
$.cjax({ $.cjax({
url: "/api/sso/enable", url: '/api/sso/Authelia',
method: "POST", method: 'GET',
dataType: 'json',
success: function(data) {
$('#autheliaServerUrl').val(data.autheliaURL);
$('#useHttps').prop('checked', data.useHTTPS);
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching SSO settings:', textStatus, errorThrown);
}
});
});
function updateAutheliaSettings(){
var autheliaServerUrl = $('#autheliaServerUrl').val();
var useHttps = $('#useHttps').prop('checked');
$.cjax({
url: '/api/sso/Authelia',
method: 'POST',
data: { data: {
enable: checked autheliaURL: autheliaServerUrl,
useHTTPS: useHttps
}, },
success: function(data) { success: function(data) {
if (data.error != undefined) { if (data.error != undefined) {
msgbox("Failed to toggle SSO: " + data.error, false); $.msgbox(data.error, false);
//Unbind the event to prevent infinite loop
$("input[name=enableOauth2]").off("change");
}else{
initSSOStatus();
}
}
});
});
});
}
initSSOStatus();
/* Save the Oauth server port */
function saveOauthServerPort(){
var port = $("input[name=oauth2Port]").val();
//Check if the port is valid
if (port < 1 || port > 65535){
msgbox("Invalid port number", false);
return; return;
} }
//Use cjax to send the port to the server with csrf token msgbox('Authelia settings updated', true);
$.cjax({ console.log('Authelia settings updated:', data);
url: "/api/sso/setPort",
method: "POST",
data: {
port: port
}, },
success: function(data) { error: function(jqXHR, textStatus, errorThrown) {
if (data.error != undefined) { console.error('Error updating Authelia settings:', textStatus, errorThrown);
msgbox("Failed to update Oauth server port: " + data.error, false);
} else {
msgbox("Oauth server port updated", true);
}
updateSSOStatus();
} }
}); });
} }
//Bind the save button to the saveOauthServerPort function
$("#saveOauthServerPortBtn").on("click", function() {
saveOauthServerPort();
});
$("input[name=oauth2Port]").on("keypress", function(e) {
if (e.which == 13) {
saveOauthServerPort();
}
});
/* Save the Oauth server URL (aka AuthURL) */
function saveAuthURL(){
var url = $("input[name=authURL]").val();
//Make sure the url contains http:// or https://
if (!url.startsWith("http://") && !url.startsWith("https://")){
msgbox("Invalid URL. Make sure to include http:// or https://", false);
$("input[name=authURL]").parent().parent().addClass("error");
return;
}else{
$("input[name=authURL]").parent().parent().removeClass("error");
}
//Use cjax to send the port to the server with csrf token
$.cjax({
url: "/api/sso/setAuthURL",
method: "POST",
data: {
"auth_url": url
},
success: function(data) {
if (data.error != undefined) {
msgbox("Failed to update Oauth server port: " + data.error, false);
} else {
msgbox("Oauth server port updated", true);
}
updateSSOStatus();
}
});
}
//Bind the save button to the saveAuthURL function
$("#saveAuthURLBtn").on("click", function() {
saveAuthURL();
});
$("input[name=authURL]").on("keypress", function(e) {
if (e.which == 13) {
saveAuthURL();
}
});
/* Registered Apps Event Handlers */
//Function to initialize the registered app table
function initRegisteredAppTable(){
$.get("/api/sso/app/list", function(data){
if(data.error != undefined){
msgbox("Failed to get registered apps: " + data.error, false);
}else{
var tbody = $("#registeredSsoApps");
tbody.empty();
for(var i = 0; i < data.length; i++){
var app = data[i];
var tr = $("<tr>");
tr.append($("<td>").text(app.AppName));
tr.append($("<td>").html('<a href="//'+app.Domain+'" target="_blank">'+app.Domain+'</a>'));
tr.append($("<td>").text(app.AppID));
tr.append($("<td>").text(app.RegisteredOn));
var removeBtn = $("<button>").addClass("ui red basic small icon button").html('<i class="ui red trash icon"></i>');
removeBtn.on("click", function(){
removeApp(app.AppID);
});
tr.append($("<td>").append(removeBtn));
tbody.append(tr);
}
if (data.length == 0){
tbody.append($("<tr>").append($("<td>").attr("colspan", 4).html(`<i class="ui green circle check icon"></i> No registered users`)));
}
}
});
}
initRegisteredAppTable();
//Also bind the refresh button to the initRegisteredAppTable function
function handleRegisterAppListRefresh(){
initRegisteredAppTable();
}
function openRegisterAppManagementSnippet(){
//Open the register app management snippet
showSideWrapper("snippet/sso_app.html");
}
//Bind the remove button to the removeApp function
function removeApp(appID){
$.cjax({
url: "/api/sso/removeApp",
method: "POST",
data: {
appID: appID
},
success: function(data){
if(data.error != undefined){
msgbox("Failed to remove app: " + data.error, false);
}else{
msgbox("App removed", true);
updateSSOStatus();
}
}
});
}
/* Registered Users Event Handlers */
function initUserList(){
$.get("/api/sso/user/list", function(data){
if(data.error != undefined){
msgbox("Failed to get registered users: " + data.error, false);
}else{
var tbody = $("#registeredSsoUsers");
tbody.empty();
for(var i = 0; i < data.length; i++){
var user = data[i];
var tr = $("<tr>");
tr.append($("<td>").text(user.Username));
tr.append($("<td>").text(user.RegisteredOn));
var resetBtn = $("<button>").addClass("ui blue basic small icon button").html('<i class="ui blue key icon"></i>');
resetBtn.on("click", function(){
resetPassword(user.Username);
});
tr.append($("<td>").append(resetBtn));
var removeBtn = $("<button>").addClass("ui red basic small icon button").html('<i class="ui red trash icon"></i>');
removeBtn.on("click", function(){
removeUser(user.Username);
});
tr.append($("<td>").append(removeBtn));
tbody.append(tr);
}
if (data.length == 0){
tbody.append($("<tr>").append($("<td>").attr("colspan", 4).html(`<i class="ui green circle check icon"></i> No registered users`)));
}
}
});
}
//Bind the refresh button to the initUserList function
function handleUserListRefresh(){
initUserList();
}
function openRegisteredUserManager(){
//Open the registered user management snippet
showSideWrapper("snippet/sso_user.html");
}
</script> </script>
-->

View File

@ -1,3 +1,10 @@
<style>
#redirect.disabled{
opacity: 0.7;
pointer-events: none;
user-select: none;
}
</style>
<div class="ui stackable grid"> <div class="ui stackable grid">
<div class="ten wide column serverstatusWrapper"> <div class="ten wide column serverstatusWrapper">
<div id="serverstatus" class="ui statustab inverted segment"> <div id="serverstatus" class="ui statustab inverted segment">
@ -362,9 +369,11 @@
} }
if (enabled){ if (enabled){
//$("#redirect").show(); //$("#redirect").show();
$("#redirect").removeClass("disabled");
msgbox("Port 80 listener enabled"); msgbox("Port 80 listener enabled");
}else{ }else{
//$("#redirect").hide(); //$("#redirect").hide();
$("#redirect").addClass("disabled");
msgbox("Port 80 listener disabled"); msgbox("Port 80 listener disabled");
} }
} }
@ -402,9 +411,11 @@
$.get("/api/proxy/listenPort80", function(data){ $.get("/api/proxy/listenPort80", function(data){
if (data){ if (data){
$("#listenP80").checkbox("set checked"); $("#listenP80").checkbox("set checked");
$("#redirect").removeClass("disabled");
//$("#redirect").show(); //$("#redirect").show();
}else{ }else{
$("#listenP80").checkbox("set unchecked"); $("#listenP80").checkbox("set unchecked");
$("#redirect").addClass("disabled");
//$("#redirect").hide(); //$("#redirect").hide();
} }

View File

@ -65,6 +65,8 @@ body{
height: calc(100% - 51px); height: calc(100% - 51px);
overflow-y: auto; overflow-y: auto;
width: 240px; width: 240px;
position: sticky;
top: 4em;
} }
.contentWindow{ .contentWindow{

View File

@ -67,6 +67,7 @@
<button id="renewNowBtn" onclick="renewNow();" class="ui basic right floated button" style="margin-top: -2em;"><i class="yellow refresh icon"></i> Renew Now</button> <button id="renewNowBtn" onclick="renewNow();" class="ui basic right floated button" style="margin-top: -2em;"><i class="yellow refresh icon"></i> Renew Now</button>
<div class="ui horizontal divider"> OR </div> <div class="ui horizontal divider"> OR </div>
<p>Select the certificates to automatic renew in the list below</p> <p>Select the certificates to automatic renew in the list below</p>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table id="domainCertFileTable" class="ui very compact unstackable basic disabled table"> <table id="domainCertFileTable" class="ui very compact unstackable basic disabled table">
<thead> <thead>
<tr> <tr>
@ -77,6 +78,7 @@
</thead> </thead>
<tbody id="domainTableBody"></tbody> <tbody id="domainTableBody"></tbody>
</table> </table>
</div>
<small><i class="ui red info circle icon"></i> Domain in red are expired</small><br> <small><i class="ui red info circle icon"></i> Domain in red are expired</small><br>
<div class="ui yellow message"> <div class="ui yellow message">
Certificate Renew only works on the certification authority (CA) supported by Zoraxy. Check Zoraxy wiki for more information on supported list of CAs. Certificate Renew only works on the certification authority (CA) supported by Zoraxy. Check Zoraxy wiki for more information on supported list of CAs.

View File

@ -364,7 +364,7 @@
method: "POST", method: "POST",
data: { data: {
"domain": editingEndpoint.ep, "domain": editingEndpoint.ep,
"maxage": 31536000 "maxage": HSTSEnabled?31536000:0,
}, },
success: function(data){ success: function(data){
if (data.error != undefined){ if (data.error != undefined){

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
<script src="../script/utils.js"></script>
<style>
body{
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<link rel="stylesheet" href="../darktheme.css">
<script src="../script/darktheme.js"></script>
<br>
<div class="ui container">
<div class="ui basic segment">
<h2 class="ui header">SSO App Management</h2>
<div class="ui divider"></div>
<h3>Work in progress</h3>
</div>
</div>
</body>
</html>

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
<script src="../script/utils.js"></script>
<style>
body{
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<link rel="stylesheet" href="../darktheme.css">
<script src="../script/darktheme.js"></script>
<br>
<div class="ui container">
<div class="ui basic segment">
<h2 class="ui header">SSO User Management</h2>
<div class="ui divider"></div>
<h3>Work in progress</h3>
</div>
</div>
</body>
</html>

View File

@ -90,7 +90,7 @@ func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Ta
UptimeTargets := []*uptime.Target{} UptimeTargets := []*uptime.Target{}
for hostid, target := range hosts { for hostid, target := range hosts {
if target.Disabled { if target.Disabled || target.DisableUptimeMonitor {
//Skip those proxy rules that is disabled //Skip those proxy rules that is disabled
continue continue
} }