mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-22 07:23:04 +02:00
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:
commit
85422c0a74
@ -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"
|
||||||
|
@ -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. |
|
||||||
|
@ -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" \
|
||||||
|
20
src/api.go
20
src/api.go
@ -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)
|
||||||
|
@ -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{
|
||||||
{
|
{
|
||||||
@ -141,14 +147,12 @@ func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
|
|||||||
Weight: 0,
|
Weight: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
InactiveOrigins: []*loadbalance.Upstream{},
|
InactiveOrigins: []*loadbalance.Upstream{},
|
||||||
BypassGlobalTLS: false,
|
BypassGlobalTLS: false,
|
||||||
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
||||||
RequireBasicAuth: false,
|
AuthenticationProvider: defaultAuth,
|
||||||
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
|
DefaultSiteOption: dynamicproxy.DefaultSite_InternalStaticWebServer,
|
||||||
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
|
DefaultSiteValue: "",
|
||||||
DefaultSiteOption: dynamicproxy.DefaultSite_InternalStaticWebServer,
|
|
||||||
DefaultSiteValue: "",
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -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 {
|
||||||
|
12
src/def.go
12
src/def.go
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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=
|
||||||
|
40
src/main.go
40
src/main.go
@ -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
|
||||||
|
@ -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
|
|
||||||
}
|
|
136
src/mod/auth/sso/authelia/authelia.go
Normal file
136
src/mod/auth/sso/authelia/authelia.go
Normal 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
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
package sso
|
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
||||||
}
|
|
@ -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: "",
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,39 +51,33 @@ func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
|
|||||||
err := sysdb.DropTable("MyTable")
|
err := sysdb.DropTable("MyTable")
|
||||||
*/
|
*/
|
||||||
|
|
||||||
func (d *Database) UpdateReadWriteMode(readOnly bool) {
|
// Create a new table
|
||||||
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
|
|
||||||
func (d *Database) NewTable(tableName string) error {
|
func (d *Database) NewTable(tableName string) error {
|
||||||
return d.newTable(tableName)
|
return d.newTable(tableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Check is table exists
|
// Check is table exists
|
||||||
func (d *Database) TableExists(tableName string) bool {
|
func (d *Database) TableExists(tableName string) bool {
|
||||||
return d.tableExists(tableName)
|
return d.tableExists(tableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Drop the given table
|
// Drop the given table
|
||||||
func (d *Database) DropTable(tableName string) error {
|
func (d *Database) DropTable(tableName string) error {
|
||||||
return d.dropTable(tableName)
|
return d.dropTable(tableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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 {
|
||||||
return d.write(tableName, key, value)
|
return d.write(tableName, key, value)
|
||||||
@ -81,14 +97,21 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Delete a value from the database table given tablename and key
|
Delete a value from the database table given tablename and key
|
||||||
|
|
||||||
err := sysdb.Delete("MyTable", "username/message");
|
err := sysdb.Delete("MyTable", "username/message");
|
||||||
*/
|
*/
|
||||||
func (d *Database) Delete(tableName string, key string) error {
|
func (d *Database) Delete(tableName string, key string) error {
|
||||||
return d.delete(tableName, key)
|
return d.delete(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()
|
||||||
}
|
}
|
||||||
|
@ -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 &Database{
|
||||||
return tx.ForEach(func(name []byte, _ *bolt.Bucket) error {
|
Db: nil,
|
||||||
tableMap.Store(string(name), "")
|
BackendType: backendType,
|
||||||
return nil
|
Backend: db,
|
||||||
})
|
}, err
|
||||||
})
|
}
|
||||||
|
|
||||||
|
db, err := dbbolt.NewBoltDatabase(dbfile)
|
||||||
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
|
|
||||||
func (d *Database) dump(filename string) ([]string, error) {
|
|
||||||
results := []string{}
|
|
||||||
|
|
||||||
d.Tables.Range(func(tableName, v interface{}) bool {
|
|
||||||
entries, err := d.ListTable(tableName.(string))
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
141
src/mod/database/dbbolt/dbbolt.go
Normal file
141
src/mod/database/dbbolt/dbbolt.go
Normal 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()
|
||||||
|
}
|
67
src/mod/database/dbbolt/dbbolt_test.go
Normal file
67
src/mod/database/dbbolt/dbbolt_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
39
src/mod/database/dbinc/dbinc.go
Normal file
39
src/mod/database/dbinc/dbinc.go
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
152
src/mod/database/dbleveldb/dbleveldb.go
Normal file
152
src/mod/database/dbleveldb/dbleveldb.go
Normal 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()
|
||||||
|
}
|
141
src/mod/database/dbleveldb/dbleveldb_test.go
Normal file
141
src/mod/database/dbleveldb/dbleveldb_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -83,22 +83,11 @@ 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
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
108
src/mod/dynamicproxy/authProviders.go
Normal file
108
src/mod/dynamicproxy/authProviders.go
Normal 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)
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
@ -143,9 +144,11 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
|
|||||||
}
|
}
|
||||||
h.Parent.logRequest(r, true, 101, "host-websocket", selectedUpstream.OriginIpOrDomain)
|
h.Parent.logRequest(r, true, 101, "host-websocket", selectedUpstream.OriginIpOrDomain)
|
||||||
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
|
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
|
||||||
SkipTLSValidation: selectedUpstream.SkipCertValidations,
|
SkipTLSValidation: selectedUpstream.SkipCertValidations,
|
||||||
SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck,
|
SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck,
|
||||||
Logger: h.Parent.Option.Logger,
|
CopyAllHeaders: true,
|
||||||
|
UserDefinedHeaders: target.HeaderRewriteRules.UserDefinedHeaders,
|
||||||
|
Logger: h.Parent.Option.Logger,
|
||||||
})
|
})
|
||||||
wspHandler.ServeHTTP(w, r)
|
wspHandler.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -221,9 +224,11 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
|||||||
}
|
}
|
||||||
h.Parent.logRequest(r, true, 101, "vdir-websocket", target.Domain)
|
h.Parent.logRequest(r, true, 101, "vdir-websocket", target.Domain)
|
||||||
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
|
||||||
Logger: h.Parent.Option.Logger,
|
CopyAllHeaders: true,
|
||||||
|
UserDefinedHeaders: target.parent.HeaderRewriteRules.UserDefinedHeaders,
|
||||||
|
Logger: h.Parent.Option.Logger,
|
||||||
})
|
})
|
||||||
wspHandler.ServeHTTP(w, r)
|
wspHandler.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,14 +33,17 @@ type ProxyHandler struct {
|
|||||||
|
|
||||||
/* Router Object Options */
|
/* Router Object Options */
|
||||||
type RouterOption struct {
|
type RouterOption struct {
|
||||||
HostUUID string //The UUID of Zoraxy, use for heading mod
|
/* Basic Settings */
|
||||||
HostVersion string //The version of Zoraxy, use for heading mod
|
HostUUID string //The UUID of Zoraxy, use for heading mod
|
||||||
Port int //Incoming port
|
HostVersion string //The version of Zoraxy, use for heading mod
|
||||||
UseTls bool //Use TLS to serve incoming requsts
|
Port int //Incoming port
|
||||||
ForceTLSLatest bool //Force TLS1.2 or above
|
UseTls bool //Use TLS to serve incoming requsts
|
||||||
NoCache bool //Force set Cache-Control: no-store
|
ForceTLSLatest bool //Force TLS1.2 or above
|
||||||
ListenOnPort80 bool //Enable port 80 http listener
|
NoCache bool //Force set Cache-Control: no-store
|
||||||
ForceHttpsRedirect bool //Force redirection of http to https endpoint
|
ListenOnPort80 bool //Enable port 80 http listener
|
||||||
|
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
|
|
||||||
Logger *logger.Logger //Logger for reverse proxy requets
|
/* Authentication Providers */
|
||||||
|
AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
|
||||||
|
|
||||||
|
/* Utilities */
|
||||||
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -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,16 +25,17 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
@ -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
@ -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
56
src/mod/geodb/updater.go
Normal 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)
|
||||||
|
}
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
215
src/mod/statistic/statistic_test.go
Normal file
215
src/mod/statistic/statistic_test.go
Normal 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)
|
||||||
|
}
|
@ -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
|
||||||
|
24
src/mod/update/updateutil/updateutil.go
Normal file
24
src/mod/update/updateutil/updateutil.go
Normal 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
|
||||||
|
}
|
50
src/mod/update/v315/typedef314.go
Normal file
50
src/mod/update/v315/typedef314.go
Normal 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
|
||||||
|
}
|
106
src/mod/update/v315/typedef315.go
Normal file
106
src/mod/update/v315/typedef315.go
Normal 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
124
src/mod/update/v315/v315.go
Normal 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
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -56,9 +57,11 @@ type WebsocketProxy struct {
|
|||||||
|
|
||||||
// Additional options for websocket proxy runtime
|
// Additional options for websocket proxy runtime
|
||||||
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
|
||||||
Logger *logger.Logger //Logger, can be nil
|
CopyAllHeaders bool //Copy all headers from incoming request to backend request
|
||||||
|
UserDefinedHeaders []*rewrite.UserDefinedHeader //User defined headers
|
||||||
|
Logger *logger.Logger //Logger, can be nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyHandler returns a new http.Handler interface that reverse proxies the
|
// ProxyHandler returns a new http.Handler interface that reverse proxies the
|
||||||
@ -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
|
||||||
|
@ -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,13 +348,16 @@ 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
|
||||||
DefaultSiteOption: 0,
|
HeaderRewriteRules: &thisCustomHeaderRules,
|
||||||
DefaultSiteValue: "",
|
|
||||||
|
//Default Site
|
||||||
|
DefaultSiteOption: 0,
|
||||||
|
DefaultSiteValue: "",
|
||||||
// Rate Limit
|
// Rate Limit
|
||||||
RequireRateLimit: requireRateLimit,
|
RequireRateLimit: requireRateLimit,
|
||||||
RateLimit: int64(proxyRateLimit),
|
RateLimit: int64(proxyRateLimit),
|
||||||
@ -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)
|
||||||
|
77
src/start.go
77
src/start.go
@ -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,
|
Logger: SystemWideLogger,
|
||||||
AuthURL: "http://auth.localhost",
|
Database: sysdb,
|
||||||
Database: sysdb,
|
})
|
||||||
Logger: SystemWideLogger,
|
|
||||||
})
|
|
||||||
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()
|
||||||
|
}
|
||||||
|
@ -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,12 +276,8 @@
|
|||||||
</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 = "";
|
||||||
if (skipWebSocketOriginCheck){
|
if (skipWebSocketOriginCheck){
|
||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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>
|
|
||||||
<!--
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
</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.
|
<p>Please note that this feature is still in development and may not work as expected.</p>
|
||||||
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 class="ui divider"></div>
|
|
||||||
<div>
|
|
||||||
<h3 class="ui header">
|
|
||||||
<i class="ui blue user circle icon"></i>
|
|
||||||
<div class="content">
|
|
||||||
Registered Users
|
|
||||||
<div class="sub header">A list of users that are registered with the SSO server</div>
|
|
||||||
</div>
|
|
||||||
</h3>
|
|
||||||
<table class="ui celled table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Username</th>
|
|
||||||
<th>Registered On</th>
|
|
||||||
<th>Reset Password</th>
|
|
||||||
<th>Remove</th>
|
|
||||||
</tr>
|
|
||||||
</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 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>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div class="ui basic segment">
|
||||||
|
<h3>Authelia</h3>
|
||||||
|
<p>Configuration settings for Authelia authentication provider.</p>
|
||||||
|
|
||||||
|
<form class="ui form">
|
||||||
|
<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 class="field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input type="checkbox" id="useHttps" name="useHttps">
|
||||||
|
<label for="useHttps">Use HTTPS</label>
|
||||||
|
<small>Check this if your authelia server uses HTTPS</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="ui basic button" onclick="event.preventDefault(); updateAutheliaSettings();"><i class="green check icon"></i> Apply Change</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$("input[name=oauth2Port]").on("change", function() {
|
$(document).ready(function() {
|
||||||
$(".ssoPort").text($(this).val());
|
$.cjax({
|
||||||
});
|
url: '/api/sso/Authelia',
|
||||||
|
method: 'GET',
|
||||||
function updateSSOStatus(){
|
dataType: 'json',
|
||||||
$.get("/api/sso/status", function(data){
|
success: function(data) {
|
||||||
if(data.error != undefined){
|
$('#autheliaServerUrl').val(data.autheliaURL);
|
||||||
//Show error message
|
$('#useHttps').prop('checked', data.useHTTPS);
|
||||||
$(".ssoRunningState").removeClass("enabled").addClass("disabled");
|
},
|
||||||
$("#ssoRunningState .webserv_status").html('Error: '+data.error);
|
error: function(jqXHR, textStatus, errorThrown) {
|
||||||
}else{
|
console.error('Error fetching SSO settings:', textStatus, errorThrown);
|
||||||
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 updateAutheliaSettings(){
|
||||||
|
var autheliaServerUrl = $('#autheliaServerUrl').val();
|
||||||
|
var useHttps = $('#useHttps').prop('checked');
|
||||||
|
|
||||||
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({
|
|
||||||
url: "/api/sso/enable",
|
|
||||||
method: "POST",
|
|
||||||
data: {
|
|
||||||
enable: checked
|
|
||||||
},
|
|
||||||
success: function(data){
|
|
||||||
if(data.error != undefined){
|
|
||||||
msgbox("Failed to toggle SSO: " + 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;
|
|
||||||
}
|
|
||||||
//Use cjax to send the port to the server with csrf token
|
|
||||||
$.cjax({
|
$.cjax({
|
||||||
url: "/api/sso/setPort",
|
url: '/api/sso/Authelia',
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
port: port
|
autheliaURL: autheliaServerUrl,
|
||||||
|
useHTTPS: useHttps
|
||||||
},
|
},
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
if (data.error != undefined) {
|
if (data.error != undefined) {
|
||||||
msgbox("Failed to update Oauth server port: " + data.error, false);
|
$.msgbox(data.error, false);
|
||||||
} else {
|
return;
|
||||||
msgbox("Oauth server port updated", true);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
updateSSOStatus();
|
msgbox('Authelia settings updated', true);
|
||||||
}
|
console.log('Authelia settings updated:', data);
|
||||||
});
|
|
||||||
}
|
|
||||||
//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) {
|
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();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
</script>
|
||||||
//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>
|
|
||||||
-->
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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{
|
||||||
|
@ -67,16 +67,18 @@
|
|||||||
<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>
|
||||||
<table id="domainCertFileTable" class="ui very compact unstackable basic disabled table">
|
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
|
||||||
<thead>
|
<table id="domainCertFileTable" class="ui very compact unstackable basic disabled table">
|
||||||
<tr>
|
<thead>
|
||||||
<th>Domain Name</th>
|
<tr>
|
||||||
<th>Match Rule</th>
|
<th>Domain Name</th>
|
||||||
<th>Auto-Renew</th>
|
<th>Match Rule</th>
|
||||||
</tr>
|
<th>Auto-Renew</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody id="domainTableBody"></tbody>
|
</thead>
|
||||||
</table>
|
<tbody id="domainTableBody"></tbody>
|
||||||
|
</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.
|
||||||
|
@ -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){
|
||||||
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user