22 Commits
3.0.1 ... 3.0.3

Author SHA1 Message Date
176249a7d9 Merge pull request #138 from tobychui/v3.0.3
Update V3.0.3

- Updated SMTP UI for non email login username
- Fixed ACME cert store reload after cert request
- Fixed default rule not applying to default site when default site is set to proxy target
- Fixed blacklist-ip not working with CIDR bug
- Fixed minor vdir bug in tailing slash detection and redirect logic
- Added custom mdns name support (-mdnsname flag)
- Added LAN tag in statistic
2024-04-30 14:39:49 +08:00
e2a449a7bc Update blacklist.go
Fixed blacklist CIDR not working bug
2024-04-30 13:39:48 +08:00
a9695e969e Update Server.go
Fixed default site bypassing access filter bug
2024-04-30 13:25:26 +08:00
7ba997dfc2 Added support for changing mdns name
+ Added `mdnsname` startup flag
2024-04-30 11:49:34 +08:00
d00117e878 Merge pull request #135 from PassiveLemon/Graceful-Shutdown
Fix: Graceful container shutdown
2024-04-29 09:18:03 +08:00
43a84a3f1c Fix: Graceful container shutdown 2024-04-28 10:55:45 -04:00
e24f31bdef Fixed #126
- Added cert store hot reload to fix newly ssl cert not loaded
- Optimized SMTP structure and UI
2024-04-28 22:25:05 +08:00
fc9240fbac Fixed #131
- Added LAN detection in geoip resolver
- Updated UI for LAN/loopback request origin rendering
2024-04-28 11:27:00 +08:00
e0f5431215 Fixed #129
- Removed requirements for Domain (now domain field can be empty and no error will be shown)
2024-04-27 22:37:55 +08:00
de658a3c6c Minor bug fix
- Added potential fix for #130
- Added fix for disabled virtual directory check (future features)
- Updated version number to v3.0.3
2024-04-26 22:40:27 +08:00
73276b1918 Update README.md
Fixed description that easily cause misunderstanding
2024-04-26 22:36:12 +08:00
abdb7d4d75 Merge pull request #125 from Morethanevil/main
Update CHANGELOG.md
2024-04-24 23:55:23 +08:00
72299ace15 Update CHANGELOG.md 2024-04-24 17:51:36 +02:00
4d6c79f51b Update README.md
Added Alias support in Features
2024-04-24 16:17:47 +08:00
2c045f4f40 Merge pull request #124 from tobychui/v3.0.2
V3.0.2 Updates

Pre-checks on git.hkwtc is working and approved

- Added alias for HTTP proxy host names
- Added separator support for create new proxy rules (use "," to add alias when creating new proxy rule)
- Added HTTP proxy host based access rules
- Added EAD Configuration for ACME (by @yeungalan )
- Fixed bug for bypassGlobalTLS endpoint do not support basic-auth
- Removed dependencies on management panel css for online font files
2024-04-24 16:15:53 +08:00
b8cf046ca6 Fixed offline font bug
- Fixed offline font bug
- Set to pre-release embedded webui
2024-04-24 11:34:00 +08:00
026dd6b89d Update README.md
Added more info
2024-04-19 09:57:34 +08:00
5805fe6ed2 Update README.md
Added more info
2024-04-19 09:56:58 +08:00
3c78211800 Added alias support
+ Added alias support (use , when adding a new proxy target to automatically add alias hostnames)
+ Fixed some UI issues
2024-04-16 23:33:24 +08:00
8e648a8e1f v3.0.2 init commit
+ Fixed zeroSSL bug (said by @yeungalan ) #45
+ Fixed manual renew button bug
+ Seperated geodb module with access controller
+ Added per hosts access control (experimental) #69
+ Fixed basic auth not working on TLS bypass mode bug
+ Fixed empty domain crash bug #120
2024-04-14 19:37:01 +08:00
a000893dd1 Merge pull request #118 from Morethanevil/main
Update CHANGELOG.md
2024-04-04 18:44:09 +08:00
db88bfb752 Update CHANGELOG.md
Thanks again for your hard work
2024-04-04 11:54:38 +02:00
46 changed files with 3219 additions and 1323 deletions

View File

@ -1,3 +1,29 @@
# v3.0.2 Apr 24 2024
+ Added alias for HTTP proxy host names [#76](https://github.com/tobychui/zoraxy/issues/76)
+ Added separator support for create new proxy rules (use "," to add alias when creating new proxy rule)
+ Added HTTP proxy host based access rules [#69](https://github.com/tobychui/zoraxy/issues/69)
+ Added EAD Configuration for ACME (by [yeungalan](https://github.com/yeungalan)) [#45](https://github.com/tobychui/zoraxy/issues/45)
+ Fixed bug for bypassGlobalTLS endpoint do not support basic-auth
+ Fixed panic due to empty domain field in json config [#120](https://github.com/tobychui/zoraxy/issues/120)
+ Removed dependencies on management panel css for online font files
# v3.0.1 Apr 04 2024
## Bugfixupdate for big release of V3, read update notes from V3 if you are still on V2
+ Added regex support for redirect (slow, don't use it unless you really needs it) [#42](https://github.com/tobychui/zoraxy/issues/42)
+ Added new dpcore implementations for faster proxy speed
+ Added support for CF-Connecting-IP to X-Real-IP auto rewrite [#114](https://github.com/tobychui/zoraxy/issues/114)
+ Added enable / disable of HTTP proxy rules in runtime via slider [#108](https://github.com/tobychui/zoraxy/issues/108)
+ Added better 404 page
+ Added option to bypass websocket origin check [#107](https://github.com/tobychui/zoraxy/issues/107)
+ Updated project homepage design
+ Fixed recursive port detection logic
+ Fixed UserAgent in resp bug
+ Updated minimum required Go version to v1.22 (Notes: Windows 7 support is dropped) [#112](https://github.com/tobychui/zoraxy/issues/112)
# v3.0.0 Feb 18 2024 # v3.0.0 Feb 18 2024
## IMPORTANT: V3 is a big rewrite and it is incompatible with V2! There is NO migration, if you want to stay on V2, please use V2 branch! ## IMPORTANT: V3 is a big rewrite and it is incompatible with V2! There is NO migration, if you want to stay on V2, please use V2 branch!

View File

@ -9,15 +9,16 @@ General purpose request (reverse) proxy and forwarding tool for networking noobs
### Features ### Features
- Simple to use interface with detail in-system instructions - Simple to use interface with detail in-system instructions
- Reverse Proxy - Reverse Proxy (HTTP/2)
- Virtual Directory - Virtual Directory
- WebSocket Proxy (automatic, no set-up needed)
- Basic Auth - Basic Auth
- Alias Hostnames
- Custom Headers - Custom Headers
- Redirection Rules - Redirection Rules
- TLS / SSL setup and deploy - TLS / SSL setup and deploy
- ACME features like auto-renew to serve your sites in http**s** - ACME features like auto-renew to serve your sites in http**s**
- SNI support (one certificate contains multiple host names) - SNI support (and SAN certs)
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners) - Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
- Global Area Network Controller Web UI (ZeroTier not included) - Global Area Network Controller Web UI (ZeroTier not included)
- TCP Tunneling / Proxy - TCP Tunneling / Proxy

View File

@ -8,10 +8,7 @@ RUN mkdir -p /opt/zoraxy/source/ &&\
mkdir -p /opt/zoraxy/config/ &&\ mkdir -p /opt/zoraxy/config/ &&\
mkdir -p /usr/local/bin/ mkdir -p /usr/local/bin/
COPY entrypoint.sh /opt/zoraxy/ RUN chmod -R 770 /opt/zoraxy/
RUN chmod -R 755 /opt/zoraxy/ &&\
chmod +x /opt/zoraxy/entrypoint.sh
VOLUME [ "/opt/zoraxy/config/" ] VOLUME [ "/opt/zoraxy/config/" ]
@ -24,15 +21,15 @@ RUN go mod tidy &&\
go build -o /usr/local/bin/zoraxy &&\ go build -o /usr/local/bin/zoraxy &&\
rm -r /opt/zoraxy/source/ rm -r /opt/zoraxy/source/
RUN chmod +x /usr/local/bin/zoraxy RUN chmod 755 /usr/local/bin/zoraxy &&\
chmod +x /usr/local/bin/zoraxy
WORKDIR /opt/zoraxy/config/ WORKDIR /opt/zoraxy/config/
ENV VERSION=$VERSION ENV VERSION=$VERSION
ENV ARGS="-noauth=false" ENV ARGS="-noauth=false"
ENTRYPOINT ["/opt/zoraxy/entrypoint.sh"] ENTRYPOINT "zoraxy" "-port=:8000" "${ARGS}"
HEALTHCHECK --interval=5s --timeout=5s --retries=2 CMD nc -vz 127.0.0.1 8000 || exit 1 HEALTHCHECK --interval=5s --timeout=5s --retries=2 CMD nc -vz 127.0.0.1 8000 || exit 1

View File

@ -1,4 +0,0 @@
#!/usr/bin/env bash
echo "Zoraxy version $VERSION"
zoraxy -port=:8000 ${ARGS}

View File

@ -3,9 +3,12 @@ package main
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strings"
"github.com/google/uuid"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
@ -17,6 +20,157 @@ import (
banning / whitelist a specific IP address or country code banning / whitelist a specific IP address or country code
*/ */
/*
General Function
*/
func handleListAccessRules(w http.ResponseWriter, r *http.Request) {
allAccessRules := accessController.ListAllAccessRules()
js, _ := json.Marshal(allAccessRules)
utils.SendJSONResponse(w, string(js))
}
func handleAttachRuleToHost(w http.ResponseWriter, r *http.Request) {
ruleid, err := utils.PostPara(r, "id")
if err != nil {
utils.SendErrorResponse(w, "invalid rule name")
return
}
host, err := utils.PostPara(r, "host")
if err != nil {
utils.SendErrorResponse(w, "invalid rule name")
return
}
//Check if access rule and proxy rule exists
targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(host)
if err != nil {
utils.SendErrorResponse(w, "invalid host given")
return
}
if !accessController.AccessRuleExists(ruleid) {
utils.SendErrorResponse(w, "access rule not exists")
return
}
//Update the proxy host acess rule id
targetProxyEndpoint.AccessFilterUUID = ruleid
targetProxyEndpoint.UpdateToRuntime()
err = SaveReverseProxyConfig(targetProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Create a new access rule, require name and desc only
func handleCreateAccessRule(w http.ResponseWriter, r *http.Request) {
ruleName, err := utils.PostPara(r, "name")
if err != nil {
utils.SendErrorResponse(w, "invalid rule name")
return
}
ruleDesc, _ := utils.PostPara(r, "desc")
//Filter out injection if any
p := bluemonday.StripTagsPolicy()
ruleName = p.Sanitize(ruleName)
ruleDesc = p.Sanitize(ruleDesc)
ruleUUID := uuid.New().String()
newAccessRule := access.AccessRule{
ID: ruleUUID,
Name: ruleName,
Desc: ruleDesc,
BlacklistEnabled: false,
WhitelistEnabled: false,
}
//Add it to runtime
err = accessController.AddNewAccessRule(&newAccessRule)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Handle removing an access rule. All proxy endpoint using this rule will be
// set to use the default rule
func handleRemoveAccessRule(w http.ResponseWriter, r *http.Request) {
ruleID, err := utils.PostPara(r, "id")
if err != nil {
utils.SendErrorResponse(w, "invalid rule id given")
return
}
if ruleID == "default" {
utils.SendErrorResponse(w, "default access rule cannot be removed")
return
}
ruleID = strings.TrimSpace(ruleID)
//Set all proxy hosts that use this access rule back to using "default"
allProxyEndpoints := dynamicProxyRouter.GetProxyEndpointsAsMap()
for _, proxyEndpoint := range allProxyEndpoints {
if strings.EqualFold(proxyEndpoint.AccessFilterUUID, ruleID) {
//This proxy endpoint is using the current access filter.
//set it to default
proxyEndpoint.AccessFilterUUID = "default"
proxyEndpoint.UpdateToRuntime()
err = SaveReverseProxyConfig(proxyEndpoint)
if err != nil {
SystemWideLogger.PrintAndLog("Access", "Unable to save updated proxy endpoint "+proxyEndpoint.RootOrMatchingDomain, err)
} else {
SystemWideLogger.PrintAndLog("Access", "Updated "+proxyEndpoint.RootOrMatchingDomain+" access filter to \"default\"", nil)
}
}
}
//Remove the access rule by ID
err = accessController.RemoveAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
SystemWideLogger.PrintAndLog("Access", "Access Rule "+ruleID+" removed", nil)
utils.SendOK(w)
}
// Only the name and desc, for other properties use blacklist / whitelist api
func handleUpadateAccessRule(w http.ResponseWriter, r *http.Request) {
ruleID, err := utils.PostPara(r, "id")
if err != nil {
utils.SendErrorResponse(w, "invalid rule id")
return
}
ruleName, err := utils.PostPara(r, "name")
if err != nil {
utils.SendErrorResponse(w, "invalid rule name")
return
}
ruleDesc, _ := utils.PostPara(r, "desc")
//Filter anything weird
p := bluemonday.StrictPolicy()
ruleName = p.Sanitize(ruleName)
ruleDesc = p.Sanitize(ruleDesc)
err = accessController.UpdateAccessRule(ruleID, ruleName, ruleDesc)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
/* /*
Blacklist Related Blacklist Related
*/ */
@ -28,11 +182,24 @@ func handleListBlacklisted(w http.ResponseWriter, r *http.Request) {
bltype = "country" bltype = "country"
} }
ruleID, err := utils.GetPara(r, "id")
if err != nil {
//Use default if not set
ruleID = "default"
}
//Load the target rule from access controller
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
resulst := []string{} resulst := []string{}
if bltype == "country" { if bltype == "country" {
resulst = geodbStore.GetAllBlacklistedCountryCode() resulst = rule.GetAllBlacklistedCountryCode()
} else if bltype == "ip" { } else if bltype == "ip" {
resulst = geodbStore.GetAllBlacklistedIp() resulst = rule.GetAllBlacklistedIp()
} }
js, _ := json.Marshal(resulst) js, _ := json.Marshal(resulst)
@ -47,7 +214,23 @@ func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.AddCountryCodeToBlackList(countryCode) ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
comment, _ := utils.PostPara(r, "comment")
p := bluemonday.StripTagsPolicy()
comment = p.Sanitize(comment)
//Load the target rule from access controller
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
rule.AddCountryCodeToBlackList(countryCode, comment)
utils.SendOK(w) utils.SendOK(w)
} }
@ -59,7 +242,19 @@ func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.RemoveCountryCodeFromBlackList(countryCode) ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
//Load the target rule from access controller
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
rule.RemoveCountryCodeFromBlackList(countryCode)
utils.SendOK(w) utils.SendOK(w)
} }
@ -71,7 +266,24 @@ func handleIpBlacklistAdd(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.AddIPToBlackList(ipAddr) ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
//Load the target rule from access controller
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
comment, _ := utils.GetPara(r, "comment")
p := bluemonday.StripTagsPolicy()
comment = p.Sanitize(comment)
rule.AddIPToBlackList(ipAddr, comment)
utils.SendOK(w)
} }
func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) { func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) {
@ -81,23 +293,46 @@ func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.RemoveIPFromBlackList(ipAddr) ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
//Load the target rule from access controller
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
rule.RemoveIPFromBlackList(ipAddr)
utils.SendOK(w) utils.SendOK(w)
} }
func handleBlacklistEnable(w http.ResponseWriter, r *http.Request) { func handleBlacklistEnable(w http.ResponseWriter, r *http.Request) {
enable, err := utils.PostPara(r, "enable") enable, _ := utils.PostPara(r, "enable")
ruleID, err := utils.PostPara(r, "id")
if err != nil { if err != nil {
//Return the current enabled state ruleID = "default"
currentEnabled := geodbStore.BlacklistEnabled }
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
if enable == "" {
//enable paramter not set
currentEnabled := rule.BlacklistEnabled
js, _ := json.Marshal(currentEnabled) js, _ := json.Marshal(currentEnabled)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
} else { } else {
if enable == "true" { if enable == "true" {
geodbStore.ToggleBlacklist(true) rule.ToggleBlacklist(true)
} else if enable == "false" { } else if enable == "false" {
geodbStore.ToggleBlacklist(false) rule.ToggleBlacklist(false)
} else { } else {
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted") utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
return return
@ -117,11 +352,22 @@ func handleListWhitelisted(w http.ResponseWriter, r *http.Request) {
bltype = "country" bltype = "country"
} }
resulst := []*geodb.WhitelistEntry{} ruleID, err := utils.GetPara(r, "id")
if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
resulst := []*access.WhitelistEntry{}
if bltype == "country" { if bltype == "country" {
resulst = geodbStore.GetAllWhitelistedCountryCode() resulst = rule.GetAllWhitelistedCountryCode()
} else if bltype == "ip" { } else if bltype == "ip" {
resulst = geodbStore.GetAllWhitelistedIp() resulst = rule.GetAllWhitelistedIp()
} }
js, _ := json.Marshal(resulst) js, _ := json.Marshal(resulst)
@ -136,11 +382,22 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
return return
} }
ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
comment, _ := utils.PostPara(r, "comment") comment, _ := utils.PostPara(r, "comment")
p := bluemonday.StrictPolicy() p := bluemonday.StrictPolicy()
comment = p.Sanitize(comment) comment = p.Sanitize(comment)
geodbStore.AddCountryCodeToWhitelist(countryCode, comment) rule.AddCountryCodeToWhitelist(countryCode, comment)
utils.SendOK(w) utils.SendOK(w)
} }
@ -152,7 +409,18 @@ func handleCountryWhitelistRemove(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.RemoveCountryCodeFromWhitelist(countryCode) ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
rule.RemoveCountryCodeFromWhitelist(countryCode)
utils.SendOK(w) utils.SendOK(w)
} }
@ -164,11 +432,23 @@ func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) {
return return
} }
ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
comment, _ := utils.PostPara(r, "comment") comment, _ := utils.PostPara(r, "comment")
p := bluemonday.StrictPolicy() p := bluemonday.StrictPolicy()
comment = p.Sanitize(comment) comment = p.Sanitize(comment)
geodbStore.AddIPToWhiteList(ipAddr, comment) rule.AddIPToWhiteList(ipAddr, comment)
utils.SendOK(w)
} }
func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) { func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
@ -178,23 +458,45 @@ func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.RemoveIPFromWhiteList(ipAddr) ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
rule.RemoveIPFromWhiteList(ipAddr)
utils.SendOK(w) utils.SendOK(w)
} }
func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) { func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) {
enable, err := utils.PostPara(r, "enable") enable, _ := utils.PostPara(r, "enable")
ruleID, err := utils.PostPara(r, "id")
if err != nil { if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
if enable == "" {
//Return the current enabled state //Return the current enabled state
currentEnabled := geodbStore.WhitelistEnabled currentEnabled := rule.WhitelistEnabled
js, _ := json.Marshal(currentEnabled) js, _ := json.Marshal(currentEnabled)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
} else { } else {
if enable == "true" { if enable == "true" {
geodbStore.ToggleWhitelist(true) rule.ToggleWhitelist(true)
} else if enable == "false" { } else if enable == "false" {
geodbStore.ToggleWhitelist(false) rule.ToggleWhitelist(false)
} else { } else {
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted") utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
return return

View File

@ -38,7 +38,7 @@ func initACME() *acme.ACMEHandler {
port = getRandomPort(30000) port = getRandomPort(30000)
} }
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port)) return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb)
} }
// create the special routing rule for ACME // create the special routing rule for ACME
@ -101,6 +101,7 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
} else { } else {
//This port do not support ACME //This port do not support ACME
utils.SendErrorResponse(w, "ACME renew only support web server listening on port 80 (http) or 443 (https)") utils.SendErrorResponse(w, "ACME renew only support web server listening on port 80 (http) or 443 (https)")
return
} }
//Add a 3 second delay to make sure everything is settle down //Add a 3 second delay to make sure everything is settle down
@ -109,6 +110,10 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
// Pass over to the acmeHandler to deal with the communication // Pass over to the acmeHandler to deal with the communication
acmeHandler.HandleRenewCertificate(w, r) acmeHandler.HandleRenewCertificate(w, r)
//Update the TLS cert store buffer
tlsCertManager.UpdateLoadedCertList()
//Restore original settings
if dynamicProxyRouter.Option.Port == 443 { if dynamicProxyRouter.Option.Port == 443 {
if !isForceHttpsRedirectEnabledOriginally { if !isForceHttpsRedirectEnabledOriginally {
//Default is off. Turn the redirection off //Default is off. Turn the redirection off

View File

@ -49,7 +49,9 @@ func initAPIs() {
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus) authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
authRouter.HandleFunc("/api/proxy/toggle", ReverseProxyToggleRuleSet) authRouter.HandleFunc("/api/proxy/toggle", ReverseProxyToggleRuleSet)
authRouter.HandleFunc("/api/proxy/list", ReverseProxyList) authRouter.HandleFunc("/api/proxy/list", ReverseProxyList)
authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail)
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint) authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint) authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials) authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS) authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS)
@ -87,6 +89,12 @@ func initAPIs() {
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule) authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport) authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
//Access Rules API
authRouter.HandleFunc("/api/access/list", handleListAccessRules)
authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost)
authRouter.HandleFunc("/api/access/create", handleCreateAccessRule)
authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule)
authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule)
//Blacklist APIs //Blacklist APIs
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted) authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd) authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
@ -94,7 +102,6 @@ func initAPIs() {
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd) authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove) authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable) authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
//Whitelist APIs //Whitelist APIs
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted) authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd) authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
@ -179,6 +186,7 @@ func initAPIs() {
authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA) authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA)
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail) authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains) authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB)
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains) authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy) authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow) authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)

View File

@ -25,12 +25,6 @@ func HandleSMTPSet(w http.ResponseWriter, r *http.Request) {
return return
} }
domain, err := utils.PostPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "domain cannot be empty")
return
}
portString, err := utils.PostPara(r, "port") portString, err := utils.PostPara(r, "port")
if err != nil { if err != nil {
utils.SendErrorResponse(w, "port must be a valid integer") utils.SendErrorResponse(w, "port must be a valid integer")
@ -76,7 +70,6 @@ func HandleSMTPSet(w http.ResponseWriter, r *http.Request) {
//Set the email sender properties //Set the email sender properties
thisEmailSender := email.Sender{ thisEmailSender := email.Sender{
Hostname: strings.TrimSpace(hostname), Hostname: strings.TrimSpace(hostname),
Domain: strings.TrimSpace(domain),
Port: port, Port: port,
Username: strings.TrimSpace(username), Username: strings.TrimSpace(username),
Password: strings.TrimSpace(password), Password: strings.TrimSpace(password),
@ -206,7 +199,7 @@ var (
) )
func HandleAdminAccountResetEmail(w http.ResponseWriter, r *http.Request) { func HandleAdminAccountResetEmail(w http.ResponseWriter, r *http.Request) {
if EmailSender.Username == "" || EmailSender.Domain == "" { if EmailSender.Username == "" {
//Reset account not setup //Reset account not setup
utils.SendErrorResponse(w, "Reset account not setup.") utils.SendErrorResponse(w, "Reset account not setup.")
return return

View File

@ -12,6 +12,7 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"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/database" "imuslab.com/zoraxy/mod/database"
@ -40,6 +41,7 @@ var noauth = flag.Bool("noauth", false, "Disable authentication for management i
var showver = flag.Bool("version", false, "Show version of this server") var showver = flag.Bool("version", false, "Show version of this server")
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)") var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
var allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder") var allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder")
var mdnsName = flag.String("mdnsname", "", "mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)")
var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node") var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port") var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)") var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
@ -50,7 +52,7 @@ var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
var ( var (
name = "Zoraxy" name = "Zoraxy"
version = "3.0.1" version = "3.0.3"
nodeUUID = "generic" nodeUUID = "generic"
development = false //Set this to false to use embedded web fs development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix() bootTime = time.Now().Unix()
@ -69,7 +71,8 @@ var (
tlsCertManager *tlscert.Manager //TLS / SSL management tlsCertManager *tlscert.Manager //TLS / SSL management
redirectTable *redirection.RuleTable //Handle special redirection rule sets redirectTable *redirection.RuleTable //Handle special redirection rule sets
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
geodbStore *geodb.Store //GeoIP database, also handle black list and whitelist features geodbStore *geodb.Store //GeoIP database, for resolving IP into country code
accessController *access.Controller //Access controller, handle black list and white list
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
statisticCollector *statistic.Collector //Collecting statistic from visitors statisticCollector *statistic.Collector //Collecting statistic from visitors
uptimeMonitor *uptime.Monitor //Uptime monitor service worker uptimeMonitor *uptime.Monitor //Uptime monitor service worker

221
src/mod/access/access.go Normal file
View File

@ -0,0 +1,221 @@
package access
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"sync"
"imuslab.com/zoraxy/mod/utils"
)
/*
Access.go
This module is the new version of access control system
where now the blacklist / whitelist are seperated from
geodb module
*/
// Create a new access controller to handle blacklist / whitelist
func NewAccessController(options *Options) (*Controller, error) {
sysdb := options.Database
if sysdb == nil {
return nil, errors.New("missing database access")
}
//Create the config folder if not exists
confFolder := options.ConfigFolder
if !utils.FileExists(confFolder) {
err := os.MkdirAll(confFolder, 0775)
if err != nil {
return nil, err
}
}
// Create the global access rule if not exists
var defaultAccessRule = AccessRule{
ID: "default",
Name: "Default",
Desc: "Default access rule for all HTTP proxy hosts",
BlacklistEnabled: false,
WhitelistEnabled: false,
WhiteListCountryCode: &map[string]string{},
WhiteListIP: &map[string]string{},
BlackListContryCode: &map[string]string{},
BlackListIP: &map[string]string{},
}
defaultRuleSettingFile := filepath.Join(confFolder, "default.json")
if utils.FileExists(defaultRuleSettingFile) {
//Load from file
defaultRuleBytes, err := os.ReadFile(defaultRuleSettingFile)
if err == nil {
err = json.Unmarshal(defaultRuleBytes, &defaultAccessRule)
if err != nil {
options.Logger.PrintAndLog("Access", "Unable to parse default routing rule config file. Using default", err)
}
}
} else {
//Create one
js, _ := json.MarshalIndent(defaultAccessRule, "", " ")
os.WriteFile(defaultRuleSettingFile, js, 0775)
}
//Generate a controller object
thisController := Controller{
DefaultAccessRule: &defaultAccessRule,
ProxyAccessRule: &sync.Map{},
Options: options,
}
//Assign default access rule parent
thisController.DefaultAccessRule.parent = &thisController
//Load all acccess rules from file
configFiles, err := filepath.Glob(options.ConfigFolder + "/*.json")
if err != nil {
return nil, err
}
ProxyAccessRules := sync.Map{}
for _, configFile := range configFiles {
if filepath.Base(configFile) == "default.json" {
//Skip this, as this was already loaded as default
continue
}
configContent, err := os.ReadFile(configFile)
if err != nil {
options.Logger.PrintAndLog("Access", "Unable to load config "+filepath.Base(configFile), err)
continue
}
//Parse the config file into AccessRule
thisAccessRule := AccessRule{}
err = json.Unmarshal(configContent, &thisAccessRule)
if err != nil {
options.Logger.PrintAndLog("Access", "Unable to parse config "+filepath.Base(configFile), err)
continue
}
thisAccessRule.parent = &thisController
ProxyAccessRules.Store(thisAccessRule.ID, &thisAccessRule)
}
thisController.ProxyAccessRule = &ProxyAccessRules
return &thisController, nil
}
// Get the global access rule
func (c *Controller) GetGlobalAccessRule() (*AccessRule, error) {
if c.DefaultAccessRule == nil {
return nil, errors.New("global access rule is not set")
}
return c.DefaultAccessRule, nil
}
// Load access rules to runtime, require rule ID
func (c *Controller) GetAccessRuleByID(accessRuleID string) (*AccessRule, error) {
if accessRuleID == "default" || accessRuleID == "" {
return c.DefaultAccessRule, nil
}
//Load from sync.Map, should be O(1)
targetRule, ok := c.ProxyAccessRule.Load(accessRuleID)
if !ok {
return nil, errors.New("target access rule not exists")
}
ar, ok := targetRule.(*AccessRule)
if !ok {
return nil, errors.New("assertion of access rule failed, version too old?")
}
return ar, nil
}
// Return all the access rules currently in runtime, including default
func (c *Controller) ListAllAccessRules() []*AccessRule {
results := []*AccessRule{c.DefaultAccessRule}
c.ProxyAccessRule.Range(func(key, value interface{}) bool {
results = append(results, value.(*AccessRule))
return true
})
return results
}
// Check if an access rule exists given the rule id
func (c *Controller) AccessRuleExists(ruleID string) bool {
r, _ := c.GetAccessRuleByID(ruleID)
if r != nil {
//An access rule with identical ID exists
return true
}
return false
}
// Add a new access rule to runtime and save it to file
func (c *Controller) AddNewAccessRule(newRule *AccessRule) error {
r, _ := c.GetAccessRuleByID(newRule.ID)
if r != nil {
//An access rule with identical ID exists
return errors.New("access rule already exists")
}
//Check if the blacklist and whitelist are populated with empty map
if newRule.BlackListContryCode == nil {
newRule.BlackListContryCode = &map[string]string{}
}
if newRule.BlackListIP == nil {
newRule.BlackListIP = &map[string]string{}
}
if newRule.WhiteListCountryCode == nil {
newRule.WhiteListCountryCode = &map[string]string{}
}
if newRule.WhiteListIP == nil {
newRule.WhiteListIP = &map[string]string{}
}
//Add access rule to runtime
newRule.parent = c
c.ProxyAccessRule.Store(newRule.ID, newRule)
//Save rule to file
newRule.SaveChanges()
return nil
}
// Update the access rule meta info.
func (c *Controller) UpdateAccessRule(ruleID string, name string, desc string) error {
targetAccessRule, err := c.GetAccessRuleByID(ruleID)
if err != nil {
return err
}
///Update the name and desc
targetAccessRule.Name = name
targetAccessRule.Desc = desc
//Overwrite the rule currently in sync map
if ruleID == "default" {
c.DefaultAccessRule = targetAccessRule
} else {
c.ProxyAccessRule.Store(ruleID, targetAccessRule)
}
return targetAccessRule.SaveChanges()
}
// Remove the access rule by its id
func (c *Controller) RemoveAccessRuleByID(ruleID string) error {
if !c.AccessRuleExists(ruleID) {
return errors.New("access rule not exists")
}
//Default cannot be removed
if ruleID == "default" {
return errors.New("default access rule cannot be removed")
}
//Remove it
return c.DeleteAccessRuleByID(ruleID)
}

View File

@ -0,0 +1,153 @@
package access
import (
"encoding/json"
"errors"
"net"
"os"
"path/filepath"
)
// Check both blacklist and whitelist for access for both geoIP and ip / CIDR ranges
func (s *AccessRule) AllowIpAccess(ipaddr string) bool {
if s.IsBlacklisted(ipaddr) {
return false
}
return s.IsWhitelisted(ipaddr)
}
// Check both blacklist and whitelist for access using net.Conn
func (s *AccessRule) AllowConnectionAccess(conn net.Conn) bool {
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
return s.AllowIpAccess(addr.IP.String())
}
return true
}
// Toggle black list
func (s *AccessRule) ToggleBlacklist(enabled bool) {
s.BlacklistEnabled = enabled
s.SaveChanges()
}
// Toggel white list
func (s *AccessRule) ToggleWhitelist(enabled bool) {
s.WhitelistEnabled = enabled
s.SaveChanges()
}
/*
Check if a IP address is blacklisted, in either country or IP blacklist
IsBlacklisted default return is false (allow access)
*/
func (s *AccessRule) IsBlacklisted(ipAddr string) bool {
if !s.BlacklistEnabled {
//Blacklist not enabled. Always return false
return false
}
if ipAddr == "" {
//Unable to get the target IP address
return false
}
countryCode, err := s.parent.Options.GeoDB.ResolveCountryCodeFromIP(ipAddr)
if err != nil {
return false
}
if s.IsCountryCodeBlacklisted(countryCode.CountryIsoCode) {
return true
}
if s.IsIPBlacklisted(ipAddr) {
return true
}
return false
}
/*
IsWhitelisted check if a given IP address is in the current
server's white list.
Note that the Whitelist default result is true even
when encountered error
*/
func (s *AccessRule) IsWhitelisted(ipAddr string) bool {
if !s.WhitelistEnabled {
//Whitelist not enabled. Always return true (allow access)
return true
}
if ipAddr == "" {
//Unable to get the target IP address, assume ok
return true
}
countryCode, err := s.parent.Options.GeoDB.ResolveCountryCodeFromIP(ipAddr)
if err != nil {
return true
}
if s.IsCountryCodeWhitelisted(countryCode.CountryIsoCode) {
return true
}
if s.IsIPWhitelisted(ipAddr) {
return true
}
return false
}
/* Utilities function */
// Update the current access rule to json file
func (s *AccessRule) SaveChanges() error {
if s.parent == nil {
return errors.New("save failed: access rule detached from controller")
}
saveTarget := filepath.Join(s.parent.Options.ConfigFolder, s.ID+".json")
js, err := json.MarshalIndent(s, "", " ")
if err != nil {
return err
}
err = os.WriteFile(saveTarget, js, 0775)
return err
}
// Delete this access rule, this will only delete the config file.
// for runtime delete, use DeleteAccessRuleByID from parent Controller
func (s *AccessRule) DeleteConfigFile() error {
saveTarget := filepath.Join(s.parent.Options.ConfigFolder, s.ID+".json")
return os.Remove(saveTarget)
}
// Delete the access rule by given ID
func (c *Controller) DeleteAccessRuleByID(accessRuleID string) error {
targetAccessRule, err := c.GetAccessRuleByID(accessRuleID)
if err != nil {
return err
}
//Delete config file associated with this access rule
err = targetAccessRule.DeleteConfigFile()
if err != nil {
return err
}
//Delete the access rule in runtime
c.ProxyAccessRule.Delete(accessRuleID)
return nil
}
// Create a deep copy object of the access rule list
func deepCopy(valueList map[string]string) map[string]string {
result := map[string]string{}
js, _ := json.Marshal(valueList)
json.Unmarshal(js, &result)
return result
}

View File

@ -0,0 +1,94 @@
package access
import (
"strings"
"imuslab.com/zoraxy/mod/netutils"
)
/*
Blacklist.go
This script store the blacklist related functions
*/
// Geo Blacklist
func (s *AccessRule) AddCountryCodeToBlackList(countryCode string, comment string) {
countryCode = strings.ToLower(countryCode)
newBlacklistCountryCode := deepCopy(*s.BlackListContryCode)
newBlacklistCountryCode[countryCode] = comment
s.BlackListContryCode = &newBlacklistCountryCode
s.SaveChanges()
}
func (s *AccessRule) RemoveCountryCodeFromBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
newBlacklistCountryCode := deepCopy(*s.BlackListContryCode)
delete(newBlacklistCountryCode, countryCode)
s.BlackListContryCode = &newBlacklistCountryCode
s.SaveChanges()
}
func (s *AccessRule) IsCountryCodeBlacklisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
blacklistMap := *s.BlackListContryCode
_, ok := blacklistMap[countryCode]
return ok
}
func (s *AccessRule) GetAllBlacklistedCountryCode() []string {
bannedCountryCodes := []string{}
blacklistMap := *s.BlackListContryCode
for cc, _ := range blacklistMap {
bannedCountryCodes = append(bannedCountryCodes, cc)
}
return bannedCountryCodes
}
// IP Blacklsits
func (s *AccessRule) AddIPToBlackList(ipAddr string, comment string) {
newBlackListIP := deepCopy(*s.BlackListIP)
newBlackListIP[ipAddr] = comment
s.BlackListIP = &newBlackListIP
s.SaveChanges()
}
func (s *AccessRule) RemoveIPFromBlackList(ipAddr string) {
newBlackListIP := deepCopy(*s.BlackListIP)
delete(newBlackListIP, ipAddr)
s.BlackListIP = &newBlackListIP
s.SaveChanges()
}
func (s *AccessRule) GetAllBlacklistedIp() []string {
bannedIps := []string{}
blacklistMap := *s.BlackListIP
for ip, _ := range blacklistMap {
bannedIps = append(bannedIps, ip)
}
return bannedIps
}
func (s *AccessRule) IsIPBlacklisted(ipAddr string) bool {
IPBlacklist := *s.BlackListIP
_, ok := IPBlacklist[ipAddr]
if ok {
return true
}
//Check for CIDR
for ipOrCIDR, _ := range IPBlacklist {
wildcardMatch := netutils.MatchIpWildcard(ipAddr, ipOrCIDR)
if wildcardMatch {
return true
}
cidrMatch := netutils.MatchIpCIDR(ipAddr, ipOrCIDR)
if cidrMatch {
return true
}
}
return false
}

38
src/mod/access/typedef.go Normal file
View File

@ -0,0 +1,38 @@
package access
import (
"sync"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
)
type Options struct {
Logger logger.Logger
ConfigFolder string //Path for storing config files
GeoDB *geodb.Store //For resolving country code
Database *database.Database //System key-value database
}
type AccessRule struct {
ID string
Name string
Desc string
BlacklistEnabled bool
WhitelistEnabled bool
/* Whitelist Blacklist Table, value is comment if supported */
WhiteListCountryCode *map[string]string
WhiteListIP *map[string]string
BlackListContryCode *map[string]string
BlackListIP *map[string]string
parent *Controller
}
type Controller struct {
DefaultAccessRule *AccessRule
ProxyAccessRule *sync.Map
Options *Options
}

112
src/mod/access/whitelist.go Normal file
View File

@ -0,0 +1,112 @@
package access
import (
"strings"
"imuslab.com/zoraxy/mod/netutils"
)
/*
Whitelist.go
This script handles whitelist related functions
*/
const (
EntryType_CountryCode int = 0
EntryType_IP int = 1
)
type WhitelistEntry struct {
EntryType int //Entry type of whitelist, Country Code or IP
CC string //ISO Country Code
IP string //IP address or range
Comment string //Comment for this entry
}
//Geo Whitelist
func (s *AccessRule) AddCountryCodeToWhitelist(countryCode string, comment string) {
countryCode = strings.ToLower(countryCode)
newWhitelistCC := deepCopy(*s.WhiteListCountryCode)
newWhitelistCC[countryCode] = comment
s.WhiteListCountryCode = &newWhitelistCC
s.SaveChanges()
}
func (s *AccessRule) RemoveCountryCodeFromWhitelist(countryCode string) {
countryCode = strings.ToLower(countryCode)
newWhitelistCC := deepCopy(*s.WhiteListCountryCode)
delete(newWhitelistCC, countryCode)
s.WhiteListCountryCode = &newWhitelistCC
s.SaveChanges()
}
func (s *AccessRule) IsCountryCodeWhitelisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
whitelistCC := *s.WhiteListCountryCode
_, ok := whitelistCC[countryCode]
return ok
}
func (s *AccessRule) GetAllWhitelistedCountryCode() []*WhitelistEntry {
whitelistedCountryCode := []*WhitelistEntry{}
whitelistCC := *s.WhiteListCountryCode
for cc, comment := range whitelistCC {
whitelistedCountryCode = append(whitelistedCountryCode, &WhitelistEntry{
EntryType: EntryType_CountryCode,
CC: cc,
Comment: comment,
})
}
return whitelistedCountryCode
}
//IP Whitelist
func (s *AccessRule) AddIPToWhiteList(ipAddr string, comment string) {
newWhitelistIP := deepCopy(*s.WhiteListIP)
newWhitelistIP[ipAddr] = comment
s.WhiteListIP = &newWhitelistIP
s.SaveChanges()
}
func (s *AccessRule) RemoveIPFromWhiteList(ipAddr string) {
newWhitelistIP := deepCopy(*s.WhiteListIP)
delete(newWhitelistIP, ipAddr)
s.WhiteListIP = &newWhitelistIP
s.SaveChanges()
}
func (s *AccessRule) IsIPWhitelisted(ipAddr string) bool {
//Check for IP wildcard and CIRD rules
WhitelistedIP := *s.WhiteListIP
for ipOrCIDR, _ := range WhitelistedIP {
wildcardMatch := netutils.MatchIpWildcard(ipAddr, ipOrCIDR)
if wildcardMatch {
return true
}
cidrMatch := netutils.MatchIpCIDR(ipAddr, ipOrCIDR)
if cidrMatch {
return true
}
}
return false
}
func (s *AccessRule) GetAllWhitelistedIp() []*WhitelistEntry {
whitelistedIp := []*WhitelistEntry{}
currentWhitelistedIP := *s.WhiteListIP
for ipOrCIDR, comment := range currentWhitelistedIP {
thisEntry := WhitelistEntry{
EntryType: EntryType_IP,
IP: ipOrCIDR,
Comment: comment,
}
whitelistedIp = append(whitelistedIp, &thisEntry)
}
return whitelistedIp
}

View File

@ -9,6 +9,7 @@ import (
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"encoding/pem" "encoding/pem"
"errors"
"fmt" "fmt"
"log" "log"
"net" "net"
@ -24,6 +25,7 @@ import (
"github.com/go-acme/lego/v4/challenge/http01" "github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration" "github.com/go-acme/lego/v4/registration"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
@ -40,6 +42,11 @@ type ACMEUser struct {
key crypto.PrivateKey key crypto.PrivateKey
} }
type EABConfig struct {
Kid string `json:"kid"`
HmacKey string `json:"HmacKey"`
}
// GetEmail returns the email of the ACMEUser. // GetEmail returns the email of the ACMEUser.
func (u *ACMEUser) GetEmail() string { func (u *ACMEUser) GetEmail() string {
return u.Email return u.Email
@ -59,13 +66,15 @@ func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey {
type ACMEHandler struct { type ACMEHandler struct {
DefaultAcmeServer string DefaultAcmeServer string
Port string Port string
Database *database.Database
} }
// NewACME creates a new ACMEHandler instance. // NewACME creates a new ACMEHandler instance.
func NewACME(acmeServer string, port string) *ACMEHandler { func NewACME(acmeServer string, port string, database *database.Database) *ACMEHandler {
return &ACMEHandler{ return &ACMEHandler{
DefaultAcmeServer: acmeServer, DefaultAcmeServer: acmeServer,
Port: port, Port: port,
Database: database,
} }
} }
@ -143,11 +152,64 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
} }
// New users will need to register // New users will need to register
/*
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return false, err return false, err
} }
*/
var reg *registration.Resource
// New users will need to register
if client.GetExternalAccountRequired() {
log.Println("External Account Required for this ACME Provider.")
// IF KID and HmacEncoded is overidden
if !a.Database.TableExists("acme") {
a.Database.NewTable("acme")
return false, errors.New("kid and HmacEncoded configuration required for ACME Provider (Error -1)")
}
if !a.Database.KeyExists("acme", config.CADirURL+"_kid") || !a.Database.KeyExists("acme", config.CADirURL+"_hmacEncoded") {
return false, errors.New("kid and HmacEncoded configuration required for ACME Provider (Error -2)")
}
var kid string
var hmacEncoded string
err := a.Database.Read("acme", config.CADirURL+"_kid", &kid)
if err != nil {
log.Println(err)
return false, err
}
err = a.Database.Read("acme", config.CADirURL+"_hmacEncoded", &hmacEncoded)
if err != nil {
log.Println(err)
return false, err
}
log.Println("EAB Credential retrieved.", kid, hmacEncoded)
if kid != "" && hmacEncoded != "" {
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: kid,
HmacEncoded: hmacEncoded,
})
}
if err != nil {
log.Println(err)
return false, err
}
//return false, errors.New("External Account Required for this ACME Provider.")
} else {
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
log.Println(err)
return false, err
}
}
adminUser.Registration = reg adminUser.Registration = reg
// obtain the certificate // obtain the certificate

View File

@ -373,3 +373,34 @@ func (a *AutoRenewer) saveRenewConfigToFile() error {
js, _ := json.MarshalIndent(a.RenewerConfig, "", " ") js, _ := json.MarshalIndent(a.RenewerConfig, "", " ")
return os.WriteFile(a.ConfigFilePath, js, 0775) return os.WriteFile(a.ConfigFilePath, js, 0775)
} }
// Handle update auto renew EAD configuration
func (a *AutoRenewer) HanldeSetEAB(w http.ResponseWriter, r *http.Request) {
kid, err := utils.GetPara(r, "kid")
if err != nil {
utils.SendErrorResponse(w, "kid not set")
return
}
hmacEncoded, err := utils.GetPara(r, "hmacEncoded")
if err != nil {
utils.SendErrorResponse(w, "hmacEncoded not set")
return
}
acmeDirectoryURL, err := utils.GetPara(r, "acmeDirectoryURL")
if err != nil {
utils.SendErrorResponse(w, "acmeDirectoryURL not set")
return
}
if !a.AcmeHandler.Database.TableExists("acme") {
a.AcmeHandler.Database.NewTable("acme")
}
a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_kid", kid)
a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_hmacEncoded", hmacEncoded)
utils.SendOK(w)
}

View File

@ -1,76 +0,0 @@
package aroz
import (
"encoding/json"
"flag"
"fmt"
"net/http"
"net/url"
"os"
)
//To be used with arozos system
type ArozHandler struct {
Port string
restfulEndpoint string
}
//Information required for registering this subservice to arozos
type ServiceInfo struct {
Name string //Name of this module. e.g. "Audio"
Desc string //Description for this module
Group string //Group of the module, e.g. "system" / "media" etc
IconPath string //Module icon image path e.g. "Audio/img/function_icon.png"
Version string //Version of the module. Format: [0-9]*.[0-9][0-9].[0-9]
StartDir string //Default starting dir, e.g. "Audio/index.html"
SupportFW bool //Support floatWindow. If yes, floatWindow dir will be loaded
LaunchFWDir string //This link will be launched instead of 'StartDir' if fw mode
SupportEmb bool //Support embedded mode
LaunchEmb string //This link will be launched instead of StartDir / Fw if a file is opened with this module
InitFWSize []int //Floatwindow init size. [0] => Width, [1] => Height
InitEmbSize []int //Embedded mode init size. [0] => Width, [1] => Height
SupportedExt []string //Supported File Extensions. e.g. ".mp3", ".flac", ".wav"
}
//This function will request the required flag from the startup paramters and parse it to the need of the arozos.
func HandleFlagParse(info ServiceInfo) *ArozHandler {
var infoRequestMode = flag.Bool("info", false, "Show information about this program in JSON")
var port = flag.String("port", ":8000", "Management web interface listening port")
var restful = flag.String("rpt", "", "Reserved")
//Parse the flags
flag.Parse()
if *infoRequestMode {
//Information request mode
jsonString, _ := json.MarshalIndent(info, "", " ")
fmt.Println(string(jsonString))
os.Exit(0)
}
return &ArozHandler{
Port: *port,
restfulEndpoint: *restful,
}
}
//Get the username and resources access token from the request, return username, token
func (a *ArozHandler) GetUserInfoFromRequest(w http.ResponseWriter, r *http.Request) (string, string) {
username := r.Header.Get("aouser")
token := r.Header.Get("aotoken")
return username, token
}
func (a *ArozHandler) IsUsingExternalPermissionManager() bool {
return !(a.restfulEndpoint == "")
}
//Request gateway interface for advance permission sandbox control
func (a *ArozHandler) RequestGatewayInterface(token string, script string) (*http.Response, error) {
resp, err := http.PostForm(a.restfulEndpoint,
url.Values{"token": {token}, "script": {script}})
if err != nil {
// handle error
return nil, err
}
return resp, nil
}

Binary file not shown.

View File

@ -6,8 +6,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"imuslab.com/zoraxy/mod/geodb"
) )
/* /*
@ -32,14 +30,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r) matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
if matchedRoutingRule != nil { if matchedRoutingRule != nil {
//Matching routing rule found. Let the sub-router handle it //Matching routing rule found. Let the sub-router handle it
if matchedRoutingRule.UseSystemAccessControl {
//This matching rule request system access control.
//check access logic
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
}
}
matchedRoutingRule.Route(w, r) matchedRoutingRule.Route(w, r)
return return
} }
@ -47,14 +37,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
//Inject headers //Inject headers
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion) w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
/*
General Access Check
*/
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
}
/* /*
Redirection Routing Redirection Routing
*/ */
@ -65,19 +47,30 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
//Extract request host to see if it is virtual directory or subdomain /*
Host Routing
*/
//Extract request host to see if any proxy rule is matched
domainOnly := r.Host domainOnly := r.Host
if strings.Contains(r.Host, ":") { if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":") hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0] domainOnly = hostPath[0]
} }
/*
Host Routing
*/
sep := h.Parent.getProxyEndpointFromHostname(domainOnly) sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
if sep != nil && !sep.Disabled { if sep != nil && !sep.Disabled {
//Matching proxy rule found
//Access Check (blacklist / whitelist)
ruleID := sep.AccessFilterUUID
if sep.AccessFilterUUID == "" {
//Use default rule
ruleID = "default"
}
if h.handleAccessRouting(ruleID, w, r) {
//Request handled by subroute
return
}
//Validate basic auth
if sep.RequireBasicAuth { if sep.RequireBasicAuth {
err := h.handleBasicAuthRouting(w, r, sep) err := h.handleBasicAuthRouting(w, r, sep)
if err != nil { if err != nil {
@ -94,7 +87,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyType_Root { } else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyType_Root {
potentialProxtEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/") potentialProxtEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled { if potentialProxtEndpoint != nil && !potentialProxtEndpoint.Disabled {
//Missing tailing slash. Redirect to target proxy endpoint //Missing tailing slash. Redirect to target proxy endpoint
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect) http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
return return
@ -109,6 +102,13 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/* /*
Root Router Handling Root Router Handling
*/ */
//Root access control based on default rule
blocked := h.handleAccessRouting("default", w, r)
if blocked {
return
}
//Clean up the request URI //Clean up the request URI
proxyingPath := strings.TrimSpace(r.RequestURI) proxyingPath := strings.TrimSpace(r.RequestURI)
if !strings.HasSuffix(proxyingPath, "/") { if !strings.HasSuffix(proxyingPath, "/") {
@ -136,7 +136,6 @@ Once entered this routing segment, the root routing options will take over
for the routing logic. for the routing logic.
*/ */
func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) { func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) {
domainOnly := r.Host domainOnly := r.Host
if strings.Contains(r.Host, ":") { if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":") hostPath := strings.Split(r.Host, ":")
@ -203,38 +202,3 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
} }
} }
} }
// Handle access routing logic. Return true if the request is handled or blocked by the access control logic
// if the return value is false, you can continue process the response writer
func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Request) bool {
//Check if this ip is in blacklist
clientIpAddr := geodb.GetRequesterIP(r)
if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/blacklist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "blacklist", "")
return true
}
//Check if this ip is in whitelist
if !h.Parent.Option.GeodbStore.IsWhitelisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/whitelist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "whitelist", "")
return true
}
return false
}

View File

@ -0,0 +1,65 @@
package dynamicproxy
import (
"log"
"net/http"
"os"
"path/filepath"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/netutils"
)
// Handle access check (blacklist / whitelist), return true if request is handled (aka blocked)
// if the return value is false, you can continue process the response writer
func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter, r *http.Request) bool {
accessRule, err := h.Parent.Option.AccessController.GetAccessRuleByID(ruleID)
if err != nil {
//Unable to load access rule. Target rule not found?
log.Println("[Proxy] Unable to load access rule: " + ruleID)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("500 - Internal Server Error"))
return true
}
isBlocked, blockedReason := accessRequestBlocked(accessRule, h.Parent.Option.WebDirectory, w, r)
if isBlocked {
h.logRequest(r, false, 403, blockedReason, "")
}
return isBlocked
}
// Return boolean, return true if access is blocked
// For string, it will return the blocked reason (if any)
func accessRequestBlocked(accessRule *access.AccessRule, templateDirectory string, w http.ResponseWriter, r *http.Request) (bool, string) {
//Check if this ip is in blacklist
clientIpAddr := netutils.GetRequesterIP(r)
if accessRule.IsBlacklisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(templateDirectory, "templates/blacklist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
return true, "blacklist"
}
//Check if this ip is in whitelist
if !accessRule.IsWhitelisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(templateDirectory, "templates/whitelist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
return true, "whitelist"
}
//Not blocked.
return false, ""
}

View File

@ -16,6 +16,16 @@ import (
*/ */
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
err := handleBasicAuth(w, r, pe)
if err != nil {
h.logRequest(r, false, 401, "host", pe.Domain)
}
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 { if len(pe.BasicAuthExceptionRules) > 0 {
//Check if the current path matches the exception rules //Check if the current path matches the exception rules
for _, exceptionRule := range pe.BasicAuthExceptionRules { for _, exceptionRule := range pe.BasicAuthExceptionRules {
@ -44,7 +54,6 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req
} }
if !matchingFound { if !matchingFound {
h.logRequest(r, false, 401, "host", pe.Domain)
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401) w.WriteHeader(401)
return errors.New("unauthorized") return errors.New("unauthorized")

View File

@ -115,6 +115,28 @@ func (router *Router) StartProxyService() error {
r.URL, _ = url.Parse(originalHostHeader) r.URL, _ = url.Parse(originalHostHeader)
} }
//Access Check (blacklist / whitelist)
ruleID := sep.AccessFilterUUID
if sep.AccessFilterUUID == "" {
//Use default rule
ruleID = "default"
}
accessRule, err := router.Option.AccessController.GetAccessRuleByID(ruleID)
if err == nil {
isBlocked, _ := accessRequestBlocked(accessRule, router.Option.WebDirectory, w, r)
if isBlocked {
return
}
}
//Validate basic auth
if sep.RequireBasicAuth {
err := handleBasicAuth(w, r, sep)
if err != nil {
return
}
}
sep.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ sep.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: sep.Domain, ProxyDomain: sep.Domain,
OriginalHost: originalHostHeader, OriginalHost: originalHostHeader,

View File

@ -70,7 +70,8 @@ func (ep *ProxyEndpoint) AddUserDefinedHeader(key string, value string) error {
func (ep *ProxyEndpoint) GetVirtualDirectoryHandlerFromRequestURI(requestURI string) *VirtualDirectoryEndpoint { func (ep *ProxyEndpoint) GetVirtualDirectoryHandlerFromRequestURI(requestURI string) *VirtualDirectoryEndpoint {
for _, vdir := range ep.VirtualDirectories { for _, vdir := range ep.VirtualDirectories {
if strings.HasPrefix(requestURI, vdir.MatchingPath) { if strings.HasPrefix(requestURI, vdir.MatchingPath) {
return vdir thisVdir := vdir
return thisVdir
} }
} }
return nil return nil
@ -80,7 +81,8 @@ func (ep *ProxyEndpoint) GetVirtualDirectoryHandlerFromRequestURI(requestURI str
func (ep *ProxyEndpoint) GetVirtualDirectoryRuleByMatchingPath(matchingPath string) *VirtualDirectoryEndpoint { func (ep *ProxyEndpoint) GetVirtualDirectoryRuleByMatchingPath(matchingPath string) *VirtualDirectoryEndpoint {
for _, vdir := range ep.VirtualDirectories { for _, vdir := range ep.VirtualDirectories {
if vdir.MatchingPath == matchingPath { if vdir.MatchingPath == matchingPath {
return vdir thisVdir := vdir
return thisVdir
} }
} }
return nil return nil

View File

@ -11,7 +11,7 @@ import (
"strings" "strings"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/statistic" "imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/websocketproxy" "imuslab.com/zoraxy/mod/websocketproxy"
) )
@ -34,23 +34,45 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi
var targetSubdomainEndpoint *ProxyEndpoint = nil var targetSubdomainEndpoint *ProxyEndpoint = nil
ep, ok := router.ProxyEndpoints.Load(hostname) ep, ok := router.ProxyEndpoints.Load(hostname)
if ok { if ok {
//Exact hit
targetSubdomainEndpoint = ep.(*ProxyEndpoint) targetSubdomainEndpoint = ep.(*ProxyEndpoint)
if !targetSubdomainEndpoint.Disabled {
return targetSubdomainEndpoint
}
} }
//No hit. Try with wildcard //No hit. Try with wildcard and alias
matchProxyEndpoints := []*ProxyEndpoint{} matchProxyEndpoints := []*ProxyEndpoint{}
router.ProxyEndpoints.Range(func(k, v interface{}) bool { router.ProxyEndpoints.Range(func(k, v interface{}) bool {
ep := v.(*ProxyEndpoint) ep := v.(*ProxyEndpoint)
match, err := filepath.Match(ep.RootOrMatchingDomain, hostname) match, err := filepath.Match(ep.RootOrMatchingDomain, hostname)
if err != nil { if err != nil {
//Continue //Bad pattern. Skip this rule
return true return true
} }
if match { if match {
//targetSubdomainEndpoint = ep //Wildcard matches. Skip checking alias
matchProxyEndpoints = append(matchProxyEndpoints, ep) matchProxyEndpoints = append(matchProxyEndpoints, ep)
return true return true
} }
//Wildcard not match. Check for alias
if ep.MatchingDomainAlias != nil && len(ep.MatchingDomainAlias) > 0 {
for _, aliasDomain := range ep.MatchingDomainAlias {
match, err := filepath.Match(aliasDomain, hostname)
if err != nil {
//Bad pattern. Skip this alias
continue
}
if match {
//This alias match
matchProxyEndpoints = append(matchProxyEndpoints, ep)
return true
}
}
}
return true return true
}) })
@ -224,7 +246,7 @@ func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, fo
if h.Parent.Option.StatisticCollector != nil { if h.Parent.Option.StatisticCollector != nil {
go func() { go func() {
requestInfo := statistic.RequestInfo{ requestInfo := statistic.RequestInfo{
IpAddr: geodb.GetRequesterIP(r), IpAddr: netutils.GetRequesterIP(r),
RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r), RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r),
Succ: succ, Succ: succ,
StatusCode: statusCode, StatusCode: statusCode,

View File

@ -19,6 +19,9 @@ import (
func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint, error) { func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint, error) {
//Filter the tailing slash if any //Filter the tailing slash if any
domain := endpoint.Domain domain := endpoint.Domain
if len(domain) == 0 {
return nil, errors.New("invalid endpoint config")
}
if domain[len(domain)-1:] == "/" { if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1] domain = domain[:len(domain)-1]
} }
@ -51,6 +54,10 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
//Prepare proxy routing hjandler for each of the virtual directories //Prepare proxy routing hjandler for each of the virtual directories
for _, vdir := range endpoint.VirtualDirectories { for _, vdir := range endpoint.VirtualDirectories {
domain := vdir.Domain domain := vdir.Domain
if len(domain) == 0 {
//invalid vdir
continue
}
if domain[len(domain)-1:] == "/" { if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1] domain = domain[:len(domain)-1]
} }

View File

@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"sync" "sync"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection" "imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/geodb"
@ -34,7 +35,8 @@ type RouterOption struct {
ForceHttpsRedirect bool //Force redirection of http to https endpoint ForceHttpsRedirect bool //Force redirection of http to https endpoint
TlsManager *tlscert.Manager TlsManager *tlscert.Manager
RedirectRuleTable *redirection.RuleTable RedirectRuleTable *redirection.RuleTable
GeodbStore *geodb.Store //GeoIP blacklist and whitelist GeodbStore *geodb.Store //GeoIP resolver
AccessController *access.Controller //Blacklist / whitelist controller
StatisticCollector *statistic.Collector StatisticCollector *statistic.Collector
WebDirectory string //The static web server directory containing the templates folder WebDirectory string //The static web server directory containing the templates folder
} }
@ -92,6 +94,7 @@ type VirtualDirectoryEndpoint struct {
type ProxyEndpoint struct { type ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def ProxyType int //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
Domain string //Domain or IP to proxy to Domain string //Domain or IP to proxy to
//TLS/SSL Related //TLS/SSL Related
@ -111,14 +114,17 @@ type ProxyEndpoint struct {
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
//Fallback routing logic //Access Control
DefaultSiteOption int //Fallback routing logic options AccessFilterUUID string //Access filter ID
DefaultSiteValue string //Fallback routing target, optional
Disabled bool //If the rule is disabled Disabled bool //If the rule is disabled
//Fallback routing logic (Special Rule Sets Only)
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
//Internal Logic Elements //Internal Logic Elements
parent *Router parent *Router `json:"-"`
proxy *dpcore.ReverseProxy `json:"-"` proxy *dpcore.ReverseProxy `json:"-"`
} }

View File

@ -13,18 +13,16 @@ import (
type Sender struct { type Sender struct {
Hostname string //E.g. mail.gandi.net Hostname string //E.g. mail.gandi.net
Domain string //E.g. arozos.com
Port int //E.g. 587 Port int //E.g. 587
Username string //Username of the email account Username string //Username of the email account
Password string //Password of the email account Password string //Password of the email account
SenderAddr string //e.g. admin@arozos.com SenderAddr string //e.g. admin@arozos.com
} }
//Create a new email sender object // Create a new email sender object
func NewEmailSender(hostname string, domain string, port int, username string, password string, senderAddr string) *Sender { func NewEmailSender(hostname string, port int, username string, password string, senderAddr string) *Sender {
return &Sender{ return &Sender{
Hostname: hostname, Hostname: hostname,
Domain: domain,
Port: port, Port: port,
Username: username, Username: username,
Password: password, Password: password,
@ -33,13 +31,15 @@ func NewEmailSender(hostname string, domain string, port int, username string, p
} }
/* /*
Send a email to a reciving addr Send a email to a reciving addr
Example Usage: Example Usage:
SendEmail( SendEmail(
test@example.com, test@example.com,
"Free donuts", "Free donuts",
"Come get your free donuts on this Sunday!" "Come get your free donuts on this Sunday!"
)
)
*/ */
func (s *Sender) SendEmail(to string, subject string, content string) error { func (s *Sender) SendEmail(to string, subject string, content string) error {
//Parse the email content //Parse the email content
@ -50,7 +50,9 @@ func (s *Sender) SendEmail(to string, subject string, content string) error {
content + "\n\n") content + "\n\n")
//Login to the SMTP server //Login to the SMTP server
auth := smtp.PlainAuth("", s.Username+"@"+s.Domain, s.Password, s.Hostname) //Username can be username (e.g. admin) or email (e.g. admin@example.com), depending on SMTP service provider
auth := smtp.PlainAuth("", s.Username, s.Password, s.Hostname)
err := smtp.SendMail(s.Hostname+":"+strconv.Itoa(s.Port), auth, s.SenderAddr, []string{to}, msg) err := smtp.SendMail(s.Hostname+":"+strconv.Itoa(s.Port), auth, s.SenderAddr, []string{to}, msg)
if err != nil { if err != nil {
return err return err

View File

@ -1,91 +0,0 @@
package geodb
import "strings"
/*
Blacklist.go
This script store the blacklist related functions
*/
//Geo Blacklist
func (s *Store) AddCountryCodeToBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Write("blacklist-cn", countryCode, true)
}
func (s *Store) RemoveCountryCodeFromBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Delete("blacklist-cn", countryCode)
}
func (s *Store) IsCountryCodeBlacklisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
var isBlacklisted bool = false
s.sysdb.Read("blacklist-cn", countryCode, &isBlacklisted)
return isBlacklisted
}
func (s *Store) GetAllBlacklistedCountryCode() []string {
bannedCountryCodes := []string{}
entries, err := s.sysdb.ListTable("blacklist-cn")
if err != nil {
return bannedCountryCodes
}
for _, keypairs := range entries {
ip := string(keypairs[0])
bannedCountryCodes = append(bannedCountryCodes, ip)
}
return bannedCountryCodes
}
//IP Blacklsits
func (s *Store) AddIPToBlackList(ipAddr string) {
s.sysdb.Write("blacklist-ip", ipAddr, true)
}
func (s *Store) RemoveIPFromBlackList(ipAddr string) {
s.sysdb.Delete("blacklist-ip", ipAddr)
}
func (s *Store) GetAllBlacklistedIp() []string {
bannedIps := []string{}
entries, err := s.sysdb.ListTable("blacklist-ip")
if err != nil {
return bannedIps
}
for _, keypairs := range entries {
ip := string(keypairs[0])
bannedIps = append(bannedIps, ip)
}
return bannedIps
}
func (s *Store) IsIPBlacklisted(ipAddr string) bool {
var isBlacklisted bool = false
s.sysdb.Read("blacklist-ip", ipAddr, &isBlacklisted)
if isBlacklisted {
return true
}
//Check for IP wildcard and CIRD rules
AllBlacklistedIps := s.GetAllBlacklistedIp()
for _, blacklistRule := range AllBlacklistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, blacklistRule)
if wildcardMatch {
return true
}
cidrMatch := MatchIpCIDR(ipAddr, blacklistRule)
if cidrMatch {
return true
}
}
return false
}

View File

@ -2,11 +2,10 @@ package geodb
import ( import (
_ "embed" _ "embed"
"log"
"net"
"net/http" "net/http"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/netutils"
) )
//go:embed geoipv4.csv //go:embed geoipv4.csv
@ -16,8 +15,6 @@ var geoipv4 []byte //Geodb dataset for ipv4
var geoipv6 []byte //Geodb dataset for ipv6 var geoipv6 []byte //Geodb dataset for ipv6
type Store struct { type Store struct {
BlacklistEnabled bool
WhitelistEnabled bool
geodb [][]string //Parsed geodb list geodb [][]string //Parsed geodb list
geodbIpv6 [][]string //Parsed geodb list for ipv6 geodbIpv6 [][]string //Parsed geodb list for ipv6
geotrie *trie geotrie *trie
@ -48,40 +45,6 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
return nil, err return nil, err
} }
blacklistEnabled := false
whitelistEnabled := false
if sysdb != nil {
err = sysdb.NewTable("blacklist-cn")
if err != nil {
return nil, err
}
err = sysdb.NewTable("blacklist-ip")
if err != nil {
return nil, err
}
err = sysdb.NewTable("whitelist-cn")
if err != nil {
return nil, err
}
err = sysdb.NewTable("whitelist-ip")
if err != nil {
return nil, err
}
err = sysdb.NewTable("blackwhitelist")
if err != nil {
return nil, err
}
sysdb.Read("blackwhitelist", "blacklistEnabled", &blacklistEnabled)
sysdb.Read("blackwhitelist", "whitelistEnabled", &whitelistEnabled)
} else {
log.Println("Database pointer set to nil: Entering debug mode")
}
var ipv4Trie *trie var ipv4Trie *trie
if !option.AllowSlowIpv4LookUp { if !option.AllowSlowIpv4LookUp {
ipv4Trie = constrctTrieTree(parsedGeoData) ipv4Trie = constrctTrieTree(parsedGeoData)
@ -93,8 +56,6 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
} }
return &Store{ return &Store{
BlacklistEnabled: blacklistEnabled,
WhitelistEnabled: whitelistEnabled,
geodb: parsedGeoData, geodb: parsedGeoData,
geotrie: ipv4Trie, geotrie: ipv4Trie,
geodbIpv6: parsedGeoDataIpv6, geodbIpv6: parsedGeoDataIpv6,
@ -104,16 +65,6 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
}, nil }, nil
} }
func (s *Store) ToggleBlacklist(enabled bool) {
s.sysdb.Write("blackwhitelist", "blacklistEnabled", enabled)
s.BlacklistEnabled = enabled
}
func (s *Store) ToggleWhitelist(enabled bool) {
s.sysdb.Write("blackwhitelist", "whitelistEnabled", enabled)
s.WhitelistEnabled = enabled
}
func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) { func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) {
cc := s.search(ipstring) cc := s.search(ipstring)
return &CountryInfo{ return &CountryInfo{
@ -127,93 +78,16 @@ func (s *Store) Close() {
} }
/*
Check if a IP address is blacklisted, in either country or IP blacklist
IsBlacklisted default return is false (allow access)
*/
func (s *Store) IsBlacklisted(ipAddr string) bool {
if !s.BlacklistEnabled {
//Blacklist not enabled. Always return false
return false
}
if ipAddr == "" {
//Unable to get the target IP address
return false
}
countryCode, err := s.ResolveCountryCodeFromIP(ipAddr)
if err != nil {
return false
}
if s.IsCountryCodeBlacklisted(countryCode.CountryIsoCode) {
return true
}
if s.IsIPBlacklisted(ipAddr) {
return true
}
return false
}
/*
IsWhitelisted check if a given IP address is in the current
server's white list.
Note that the Whitelist default result is true even
when encountered error
*/
func (s *Store) IsWhitelisted(ipAddr string) bool {
if !s.WhitelistEnabled {
//Whitelist not enabled. Always return true (allow access)
return true
}
if ipAddr == "" {
//Unable to get the target IP address, assume ok
return true
}
countryCode, err := s.ResolveCountryCodeFromIP(ipAddr)
if err != nil {
return true
}
if s.IsCountryCodeWhitelisted(countryCode.CountryIsoCode) {
return true
}
if s.IsIPWhitelisted(ipAddr) {
return true
}
return false
}
// A helper function that check both blacklist and whitelist for access
// for both geoIP and ip / CIDR ranges
func (s *Store) AllowIpAccess(ipaddr string) bool {
if s.IsBlacklisted(ipaddr) {
return false
}
return s.IsWhitelisted(ipaddr)
}
func (s *Store) AllowConnectionAccess(conn net.Conn) bool {
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
return s.AllowIpAccess(addr.IP.String())
}
return true
}
func (s *Store) GetRequesterCountryISOCode(r *http.Request) string { func (s *Store) GetRequesterCountryISOCode(r *http.Request) string {
ipAddr := GetRequesterIP(r) ipAddr := netutils.GetRequesterIP(r)
if ipAddr == "" { if ipAddr == "" {
return "" return ""
} }
if netutils.IsPrivateIP(ipAddr) {
return "LAN"
}
countryCode, err := s.ResolveCountryCodeFromIP(ipAddr) countryCode, err := s.ResolveCountryCodeFromIP(ipAddr)
if err != nil { if err != nil {
return "" return ""

View File

@ -5,6 +5,8 @@ import (
"encoding/csv" "encoding/csv"
"io" "io"
"strings" "strings"
"imuslab.com/zoraxy/mod/netutils"
) )
func (s *Store) search(ip string) string { func (s *Store) search(ip string) string {
@ -24,7 +26,7 @@ func (s *Store) search(ip string) string {
//Search in geotrie tree //Search in geotrie tree
cc := "" cc := ""
if IsIPv6(ip) { if netutils.IsIPv6(ip) {
if s.geotrieIpv6 == nil { if s.geotrieIpv6 == nil {
cc = s.slowSearchIpv6(ip) cc = s.slowSearchIpv6(ip)
} else { } else {

View File

@ -1,129 +0,0 @@
package geodb
import (
"encoding/json"
"strings"
)
/*
Whitelist.go
This script handles whitelist related functions
*/
const (
EntryType_CountryCode int = 0
EntryType_IP int = 1
)
type WhitelistEntry struct {
EntryType int //Entry type of whitelist, Country Code or IP
CC string //ISO Country Code
IP string //IP address or range
Comment string //Comment for this entry
}
//Geo Whitelist
func (s *Store) AddCountryCodeToWhitelist(countryCode string, comment string) {
countryCode = strings.ToLower(countryCode)
entry := WhitelistEntry{
EntryType: EntryType_CountryCode,
CC: countryCode,
Comment: comment,
}
s.sysdb.Write("whitelist-cn", countryCode, entry)
}
func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Delete("whitelist-cn", countryCode)
}
func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
return s.sysdb.KeyExists("whitelist-cn", countryCode)
}
func (s *Store) GetAllWhitelistedCountryCode() []*WhitelistEntry {
whitelistedCountryCode := []*WhitelistEntry{}
entries, err := s.sysdb.ListTable("whitelist-cn")
if err != nil {
return whitelistedCountryCode
}
for _, keypairs := range entries {
thisWhitelistEntry := WhitelistEntry{}
json.Unmarshal(keypairs[1], &thisWhitelistEntry)
whitelistedCountryCode = append(whitelistedCountryCode, &thisWhitelistEntry)
}
return whitelistedCountryCode
}
//IP Whitelist
func (s *Store) AddIPToWhiteList(ipAddr string, comment string) {
thisIpEntry := WhitelistEntry{
EntryType: EntryType_IP,
IP: ipAddr,
Comment: comment,
}
s.sysdb.Write("whitelist-ip", ipAddr, thisIpEntry)
}
func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
s.sysdb.Delete("whitelist-ip", ipAddr)
}
func (s *Store) IsIPWhitelisted(ipAddr string) bool {
isWhitelisted := s.sysdb.KeyExists("whitelist-ip", ipAddr)
if isWhitelisted {
//single IP whitelist entry
return true
}
//Check for IP wildcard and CIRD rules
AllWhitelistedIps := s.GetAllWhitelistedIpAsStringSlice()
for _, whitelistRules := range AllWhitelistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, whitelistRules)
if wildcardMatch {
return true
}
cidrMatch := MatchIpCIDR(ipAddr, whitelistRules)
if cidrMatch {
return true
}
}
return false
}
func (s *Store) GetAllWhitelistedIp() []*WhitelistEntry {
whitelistedIp := []*WhitelistEntry{}
entries, err := s.sysdb.ListTable("whitelist-ip")
if err != nil {
return whitelistedIp
}
for _, keypairs := range entries {
//ip := string(keypairs[0])
thisEntry := WhitelistEntry{}
json.Unmarshal(keypairs[1], &thisEntry)
whitelistedIp = append(whitelistedIp, &thisEntry)
}
return whitelistedIp
}
func (s *Store) GetAllWhitelistedIpAsStringSlice() []string {
allWhitelistedIPs := []string{}
entries := s.GetAllWhitelistedIp()
for _, entry := range entries {
allWhitelistedIPs = append(allWhitelistedIPs, entry.IP)
}
return allWhitelistedIPs
}

View File

@ -1,4 +1,4 @@
package geodb package netutils
import ( import (
"net" "net"
@ -6,7 +6,13 @@ import (
"strings" "strings"
) )
// Utilities function /*
MatchIP.go
This script contains function for matching IP address, comparing
CIDR and IPv4 / v6 validations
*/
func GetRequesterIP(r *http.Request) string { func GetRequesterIP(r *http.Request) string {
ip := r.Header.Get("X-Real-Ip") ip := r.Header.Get("X-Real-Ip")
if ip == "" { if ip == "" {
@ -87,6 +93,10 @@ func MatchIpCIDR(ip string, cidr string) bool {
// Check if a ip is private IP range // Check if a ip is private IP range
func IsPrivateIP(ipStr string) bool { func IsPrivateIP(ipStr string) bool {
if ipStr == "127.0.0.1" || ipStr == "::1" {
//local loopback
return true
}
ip := net.ParseIP(ipStr) ip := net.ParseIP(ipStr)
if ip == nil { if ip == nil {
return false return false

View File

@ -94,6 +94,7 @@ func ReverseProxtInit() {
GeodbStore: geodbStore, GeodbStore: geodbStore,
StatisticCollector: statisticCollector, StatisticCollector: statisticCollector,
WebDirectory: *staticWebServerRoot, WebDirectory: *staticWebServerRoot,
AccessController: accessController,
}) })
if err != nil { if err != nil {
SystemWideLogger.PrintAndLog("Proxy", "Unable to create dynamic proxy router", err) SystemWideLogger.PrintAndLog("Proxy", "Unable to create dynamic proxy router", err)
@ -194,6 +195,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
useTLS := (tls == "true") useTLS := (tls == "true")
//Bypass global TLS value / allow direct access from port 80?
bypassGlobalTLS, _ := utils.PostPara(r, "bypassGlobalTLS") bypassGlobalTLS, _ := utils.PostPara(r, "bypassGlobalTLS")
if bypassGlobalTLS == "" { if bypassGlobalTLS == "" {
bypassGlobalTLS = "false" bypassGlobalTLS = "false"
@ -201,6 +203,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
useBypassGlobalTLS := bypassGlobalTLS == "true" useBypassGlobalTLS := bypassGlobalTLS == "true"
//Enable TLS validation?
stv, _ := utils.PostPara(r, "tlsval") stv, _ := utils.PostPara(r, "tlsval")
if stv == "" { if stv == "" {
stv = "false" stv = "false"
@ -208,6 +211,17 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
skipTlsValidation := (stv == "true") skipTlsValidation := (stv == "true")
//Get access rule ID
accessRuleID, _ := utils.PostPara(r, "access")
if accessRuleID == "" {
accessRuleID = "default"
}
if !accessController.AccessRuleExists(accessRuleID) {
utils.SendErrorResponse(w, "invalid access rule ID selected")
return
}
//Require basic auth?
rba, _ := utils.PostPara(r, "bauth") rba, _ := utils.PostPara(r, "bauth")
if rba == "" { if rba == "" {
rba = "false" rba = "false"
@ -254,19 +268,37 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
if eptype == "host" { if eptype == "host" {
rootOrMatchingDomain, err := utils.PostPara(r, "rootname") rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
if err != nil { if err != nil {
utils.SendErrorResponse(w, "subdomain not defined") utils.SendErrorResponse(w, "hostname not defined")
return return
} }
rootOrMatchingDomain = strings.TrimSpace(rootOrMatchingDomain)
//Check if it contains ",", if yes, split the remainings as alias
aliasHostnames := []string{}
if strings.Contains(rootOrMatchingDomain, ",") {
matchingDomains := strings.Split(rootOrMatchingDomain, ",")
if len(matchingDomains) > 1 {
rootOrMatchingDomain = matchingDomains[0]
for _, aliasHostname := range matchingDomains[1:] {
//Filter out any space
aliasHostnames = append(aliasHostnames, strings.TrimSpace(aliasHostname))
}
}
}
//Generate a proxy endpoint object
thisProxyEndpoint := dynamicproxy.ProxyEndpoint{ thisProxyEndpoint := dynamicproxy.ProxyEndpoint{
//I/O //I/O
ProxyType: dynamicproxy.ProxyType_Host, ProxyType: dynamicproxy.ProxyType_Host,
RootOrMatchingDomain: rootOrMatchingDomain, RootOrMatchingDomain: rootOrMatchingDomain,
MatchingDomainAlias: aliasHostnames,
Domain: endpoint, Domain: endpoint,
//TLS //TLS
RequireTLS: useTLS, RequireTLS: useTLS,
BypassGlobalTLS: useBypassGlobalTLS, BypassGlobalTLS: useBypassGlobalTLS,
SkipCertValidations: skipTlsValidation, SkipCertValidations: skipTlsValidation,
SkipWebSocketOriginCheck: bypassWebsocketOriginCheck, SkipWebSocketOriginCheck: bypassWebsocketOriginCheck,
AccessFilterUUID: accessRuleID,
//VDir //VDir
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{}, VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
//Custom headers //Custom headers
@ -439,6 +471,62 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
utils.SendOK(w) utils.SendOK(w)
} }
func ReverseProxyHandleAlias(w http.ResponseWriter, r *http.Request) {
rootNameOrMatchingDomain, err := utils.PostPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "Invalid ep given")
return
}
//No need to check for type as root (/) can be set to default route
//and hence, you will not need alias
//Load the previous alias from current proxy rules
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain)
if err != nil {
utils.SendErrorResponse(w, "Target proxy config not found or could not be loaded")
return
}
newAliasJSON, err := utils.PostPara(r, "alias")
if err != nil {
//No new set of alias given
utils.SendErrorResponse(w, "new alias not given")
return
}
//Write new alias to runtime and file
newAlias := []string{}
err = json.Unmarshal([]byte(newAliasJSON), &newAlias)
if err != nil {
SystemWideLogger.PrintAndLog("Proxy", "Unable to parse new alias list", err)
utils.SendErrorResponse(w, "Invalid alias list given")
return
}
//Set the current alias
newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
newProxyEndpoint.MatchingDomainAlias = newAlias
// Prepare to replace the current routing rule
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
targetProxyEntry.Remove()
dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
// Save it to file
err = SaveReverseProxyConfig(newProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, "Alias update failed")
SystemWideLogger.PrintAndLog("Proxy", "Unable to save alias update", err)
}
utils.SendOK(w)
}
func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) { func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
ep, err := utils.GetPara(r, "ep") ep, err := utils.GetPara(r, "ep")
if err != nil { if err != nil {
@ -740,6 +828,35 @@ func ReverseProxyToggleRuleSet(w http.ResponseWriter, r *http.Request) {
utils.SendOK(w) utils.SendOK(w)
} }
func ReverseProxyListDetail(w http.ResponseWriter, r *http.Request) {
eptype, err := utils.PostPara(r, "type") //Support root and host
if err != nil {
utils.SendErrorResponse(w, "type not defined")
return
}
if eptype == "host" {
epname, err := utils.PostPara(r, "epname")
if err != nil {
utils.SendErrorResponse(w, "epname not defined")
return
}
endpointRaw, ok := dynamicProxyRouter.ProxyEndpoints.Load(epname)
if !ok {
utils.SendErrorResponse(w, "proxy rule not found")
return
}
targetEndpoint := dynamicproxy.CopyEndpoint(endpointRaw.(*dynamicproxy.ProxyEndpoint))
js, _ := json.Marshal(targetEndpoint)
utils.SendJSONResponse(w, string(js))
} else if eptype == "root" {
js, _ := json.Marshal(dynamicProxyRouter.Root)
utils.SendJSONResponse(w, string(js))
} else {
utils.SendErrorResponse(w, "Invalid type given")
}
}
func ReverseProxyList(w http.ResponseWriter, r *http.Request) { func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
eptype, err := utils.PostPara(r, "type") //Support root and host eptype, err := utils.PostPara(r, "type") //Support root and host
if err != nil { if err != nil {

View File

@ -8,6 +8,7 @@ import (
"strings" "strings"
"time" "time"
"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/database" "imuslab.com/zoraxy/mod/database"
@ -91,6 +92,16 @@ func startupSequence() {
panic(err) panic(err)
} }
//Create the access controller
accessController, err = access.NewAccessController(&access.Options{
Database: sysdb,
GeoDB: geodbStore,
ConfigFolder: "./conf/access",
})
if err != nil {
panic(err)
}
//Create a statistic collector //Create a statistic collector
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{ statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
Database: sysdb, Database: sysdb,
@ -149,8 +160,17 @@ func startupSequence() {
if err != nil { if err != nil {
portInt = 8000 portInt = 8000
} }
hostName := *mdnsName
if hostName == "" {
hostName = "zoraxy_" + nodeUUID
} else {
//Trim off the suffix
hostName = strings.TrimSuffix(hostName, ".local")
}
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{ mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
HostName: "zoraxy_" + nodeUUID, HostName: hostName,
Port: portInt, Port: portInt,
Domain: "zoraxy.arozos.com", Domain: "zoraxy.arozos.com",
Model: "Network Gateway", Model: "Network Gateway",
@ -211,7 +231,7 @@ func startupSequence() {
//Create TCP Proxy Manager //Create TCP Proxy Manager
tcpProxyManager = tcpprox.NewTCProxy(&tcpprox.Options{ tcpProxyManager = tcpprox.NewTCProxy(&tcpprox.Options{
Database: sysdb, Database: sysdb,
AccessControlHandler: geodbStore.AllowConnectionAccess, AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess,
}) })
//Create WoL MAC storage table //Create WoL MAC storage table

View File

@ -1,17 +1,52 @@
<style>
#currentEditingAccessRule {
background: var(--theme_background);
color: white;
border-radius: 1em;
font-weight: bolder;
}
</style>
<div class="standardContainer"> <div class="standardContainer">
<div class="ui basic segment"> <div class="ui basic segment">
<h2>Access Control</h2> <h2>Access Control</h2>
<p>Setup blacklist or whitelist based on estimated IP geographic location or IP address</p> <p>Setup blacklist or whitelist based on estimated IP geographic location or IP address. <br>
To apply access control to a proxy hosts, create a "Access Rule" below and apply it to the proxy hosts in the HTTP Proxy tab.</p>
</div>
<div id="currentEditingAccessRule" class="ui basic segment">
Select an access rule to start editing
</div>
<div class="ui stackable grid">
<div class="four wide column">
<h4>Select a rule to configure</h4>
<p>All proxy hosts uses the "default" access rule if no rule is set.</p>
<div class="ui selection fluid dropdown" id="accessRuleSelector">
<input type="hidden" name="targetAccessRule" value="default" onchange="handleSelectedAccessRuleChange(this.value);">
<i class="dropdown icon"></i>
<div class="default text"></div>
<div class="menu" id="accessRuleList">
<div class="item" data-value="default"><i class="ui yellow star icon"></i> Default</div>
</div>
</div>
<div class="ui message">
<p id="accessRuleDesc"></p>
</div> </div>
<div class="ui top attached tabular menu"> <div class="ui divider"></div>
<div style="width: 100%;" align="center">
<button class="ui basic circular button" onclick="openAccessRuleEditor();" title="Edit Access Rules"><i class="ui edit icon"></i> Access Rule Editor</button>
<button class="ui basic circular icon button" onclick="reloadAccessRulesWithFeedback();"><i class="ui green refresh icon"></i></button>
</div>
</div>
<div class="twelve wide column">
<!-- Black / White list menu-->
<div class="ui top attached tabular menu">
<a class="accesscontrol item active" data-tab="tab_blacklist"><i class="ui red circle times icon"></i> Blacklist</a> <a class="accesscontrol item active" data-tab="tab_blacklist"><i class="ui red circle times icon"></i> Blacklist</a>
<a class="accesscontrol item" data-tab="tab_whitelist"><i class="ui green check circle icon"></i> Whitelist</a> <a class="accesscontrol item" data-tab="tab_whitelist"><i class="ui green check circle icon"></i> Whitelist</a>
<a class="accesscontrol item" data-tab="tab_quickban"><i class="ui red ban icon"></i> Quick Ban</a> <a class="accesscontrol item" data-tab="tab_quickban"><i class="ui red ban icon"></i> Quick Ban</a>
</div> </div>
<!-- Blacklist Conguration Menu--> <!-- Blacklist Conguration Menu-->
<div class="ui bottom attached tab segment active" data-tab="tab_blacklist"> <div class="ui bottom attached tab segment active" data-tab="tab_blacklist">
<h2>Blacklist</h2> <h2>Blacklist</h2>
<p>Limit access from the following country or IP address<br> <p>Limit access from the following country or IP address<br>
<small>Tips: If you only want a few regions to access your site, use whitelist instead.</small></p> <small>Tips: If you only want a few regions to access your site, use whitelist instead.</small></p>
@ -324,10 +359,10 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<!-- Whitelist Config Menu--> <!-- Whitelist Config Menu-->
<div class="ui bottom attached tab segment" data-tab="tab_whitelist"> <div class="ui bottom attached tab segment" data-tab="tab_whitelist">
<h2>Whitelist</h2> <h2>Whitelist</h2>
<p>Enable access from the following countries or IP. <br> <p>Enable access from the following countries or IP. <br>
Whitelist has lower priority than blacklist if both whitelist and blacklist are enabled <br> Whitelist has lower priority than blacklist if both whitelist and blacklist are enabled <br>
@ -646,9 +681,9 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<!-- Quick ban list--> <!-- Quick ban list-->
<div class="ui bottom attached tab segment" data-tab="tab_quickban"> <div class="ui bottom attached tab segment" data-tab="tab_quickban">
<h2>Quick Ban List</h2> <h2>Quick Ban List</h2>
<button style="margin-top: -1em;" onclick="initBlacklistQuickBanTable();" class="ui green small right floated circular basic icon button"><i class="ui refresh icon"></i></button> <button style="margin-top: -1em;" onclick="initBlacklistQuickBanTable();" class="ui green small right floated circular basic icon button"><i class="ui refresh icon"></i></button>
<p>You can perform one-click IP ban on the list below if you observe unusual traffic from an unknown origin<br> <p>You can perform one-click IP ban on the list below if you observe unusual traffic from an unknown origin<br>
@ -666,27 +701,145 @@
</tbody> </tbody>
</table> </table>
<div class="pagination"></div> <div class="pagination"></div>
</div> </div>
<div class="ui yellow message"> </div>
<i class="info circle icon"></i> Access checking and validation will slightly increase proxy latency. <br> </div>
Enabling this feature on servers with low end hardware is not recommended.
</div>
</div> </div>
<script> <script>
/* /*
Access Control Access Control
*/ */
var currentEditingAccessRule = "default"; //ID of the current editing access rule
var currentListOfAccessRules = [];
$(".dropdown").dropdown(); $(".dropdown").dropdown();
$('.menu .accesscontrol.item').tab(); $('.menu .accesscontrol.item').tab();
$('.menu .accesscontrol.item').addClass("activated"); $('.menu .accesscontrol.item').addClass("activated");
/* Access Rule Selector */
function openAccessRuleEditor(){
showSideWrapper('snippet/accessRuleEditor.html');
}
function reloadAccessRulesWithFeedback(){
reloadAccessRules(function(){
msgbox("Access Rules Reloaded", true);
});
}
function reloadAccessRules(callback=undefined){
initAccessRuleList(function(){
//Check if the previous access rule is still there
let stillExists = false;
for (var i = 0; i < currentListOfAccessRules.length; i++){
let thisAccessRule = currentListOfAccessRules[i];
if (thisAccessRule.ID == currentEditingAccessRule){
stillExists = true;
break;
}
}
if (stillExists){
handleSelectedAccessRuleChange(currentEditingAccessRule);
$("#accessRuleSelector").dropdown("set selected", currentEditingAccessRule);
}else{
handleSelectedAccessRuleChange("default");
$("#accessRuleSelector").dropdown("set selected", "default");
}
if (callback != undefined){
callback();
}
});
}
function initAccessRuleList(callback=undefined){
$.get("/api/access/list", function(data){
if (data.error == undefined){
$("#accessRuleList").html("");
data.forEach(function(rule){
let icon = `<i class="ui grey filter icon"></i>`;
if (rule.ID == "default"){
icon = `<i class="ui yellow star icon"></i>`;
}else if (rule.BlacklistEnabled && !rule.WhitelistEnabled){
//This is a blacklist filter
icon = `<i class="ui red filter icon"></i>`;
}else if (rule.WhitelistEnabled && !rule.BlacklistEnabled){
//This is a whitelist filter
icon = `<i class="ui green filter icon"></i>`;
}
$("#accessRuleList").append(`<div class="item" data-value="${rule.ID}">${icon} ${rule.Name}</div>`);
});
currentListOfAccessRules = data;
$(".dropdown").dropdown();
if (callback != undefined){
callback();
}
}else{
msgbox("Access rule load failed: " + data.error, false);
}
})
}
//Load default when the page is first loaded
initAccessRuleList(function(){
$("#accessRuleSelector").dropdown("set selected", "default");
for (var i = 0; i < currentListOfAccessRules.length; i++){
let thisAccessRule = currentListOfAccessRules[i];
if (thisAccessRule.ID == "default"){
$("#currentEditingAccessRule").html(`Editing Access Rule: ${thisAccessRule.Name} <span style="font-weight: 300;">[${thisAccessRule.ID}]</span>`);
$("#accessRuleDesc").text(thisAccessRule.Desc);
break;
}
}
initBlackWhitelistTables();
});
//On dropdown change on access rule selection
function handleSelectedAccessRuleChange(newRuleUUID){
currentEditingAccessRule = newRuleUUID;
//Load the name and desc of this acess rule
let name = "";
let desc = "";
for (var i = 0; i < currentListOfAccessRules.length; i++){
let thisAccessRule = currentListOfAccessRules[i];
if (thisAccessRule.ID == newRuleUUID){
name = thisAccessRule.Name;
desc = thisAccessRule.Desc;
break;
}
}
$("#currentEditingAccessRule").html(`Editing Access Rule: ${name} <span style="font-weight: 300;">[${newRuleUUID}]</span>`);
$("#accessRuleDesc").text(desc);
initBlackWhitelistTables();
}
//Load all the black white lists
function initBlackWhitelistTables(){
//Remove event listener on both toggle buttons
$("#enableWhitelist").off("change");
$("#enableBlacklist").off("change");
//Initialize the button states
initBlacklistEnableState();
initWhitelistEnableState();
//Update the lists
initBannedCountryList();
initIpBanTable()
initWhitelistCountryList();
initIpWhitelistTable();
}
/* /*
Table Init Table Init
*/ */
//Blacklist country table //Blacklist country table
function initBannedCountryList(){ function initBannedCountryList(){
$.get("/api/blacklist/list?type=country", function(data) { $.get("/api/blacklist/list?type=country&id=" + currentEditingAccessRule, function(data) {
let bannedListHtml = ''; let bannedListHtml = '';
data.forEach((countryCode) => { data.forEach((countryCode) => {
bannedListHtml += ` bannedListHtml += `
@ -711,11 +864,10 @@
}); });
} }
initBannedCountryList();
//Blacklist ip table //Blacklist ip table
function initIpBanTable(){ function initIpBanTable(){
$.get('/api/blacklist/list?type=ip', function(data) { $.get('/api/blacklist/list?type=ip&id=' + currentEditingAccessRule, function(data) {
$('#blacklistIpTable').html(""); $('#blacklistIpTable').html("");
if (data.length === 0) { if (data.length === 0) {
$('#blacklistIpTable').append(` $('#blacklistIpTable').append(`
@ -743,28 +895,30 @@
initBlacklistQuickBanTable(); initBlacklistQuickBanTable();
}); });
} }
initIpBanTable();
//Init blacklist state //Init blacklist state
function initBlacklistEnableState(){ function initBlacklistEnableState(){
$.get('/api/blacklist/enable', function(data){ $.get('/api/blacklist/enable?id=' + currentEditingAccessRule, function(data){
if (data == true){ if (data == true){
$('#enableBlacklist').parent().checkbox("set checked"); $('#enableBlacklist').parent().checkbox("set checked");
$("#ipTable").removeClass("disabled");
}else{ }else{
$('#enableBlacklist').parent().checkbox("set unchecked");
$("#ipTable").addClass("disabled"); $("#ipTable").addClass("disabled");
} }
//Register on change event //Register on change event
$("#enableBlacklist").on("change", function(){ $("#enableBlacklist").off("change").on("change", function(){
enableBlacklist(); enableBlacklist();
}) })
}); });
} }
initBlacklistEnableState();
//Whitelist country table //Whitelist country table
function initWhitelistCountryList(){ function initWhitelistCountryList(){
$.get("/api/whitelist/list?type=country", function(data) { $.get("/api/whitelist/list?type=country&id=" + currentEditingAccessRule, function(data) {
let bannedListHtml = ''; let bannedListHtml = '';
data.forEach((countryWhitelistEntry) => { data.forEach((countryWhitelistEntry) => {
let countryCode = countryWhitelistEntry.CC; let countryCode = countryWhitelistEntry.CC;
@ -790,11 +944,11 @@
}); });
} }
initWhitelistCountryList();
//Whitelist ip table //Whitelist ip table
function initIpWhitelistTable(){ function initIpWhitelistTable(){
$.get('/api/whitelist/list?type=ip', function(data) { $.get('/api/whitelist/list?type=ip&id=' + currentEditingAccessRule, function(data) {
$('#whitelistIpTable').html(""); $('#whitelistIpTable').html("");
if (data.length === 0) { if (data.length === 0) {
$('#whitelistIpTable').append(` $('#whitelistIpTable').append(`
@ -823,22 +977,23 @@
}); });
} }
initIpWhitelistTable();
//Init whitelist state //Init whitelist state
function initWhitelistEnableState(){ function initWhitelistEnableState(){
$.get('/api/whitelist/enable', function(data){ $.get('/api/whitelist/enable?id=' + currentEditingAccessRule, function(data){
if (data == true){ if (data == true){
$('#enableWhitelist').parent().checkbox("set checked"); $('#enableWhitelist').parent().checkbox("set checked");
}else{
$('#enableWhitelist').parent().checkbox("set unchecked");
} }
//Register on change event //Register on change event
$("#enableWhitelist").on("change", function(){ $("#enableWhitelist").off("change").on("change", function(){
enableWhitelist(); enableWhitelist();
}) })
}); });
} }
initWhitelistEnableState();
/* /*
Blacklist API Calls Blacklist API Calls
@ -848,7 +1003,7 @@
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: '/api/blacklist/enable', url: '/api/blacklist/enable',
data: { enable: isChecked }, data: { enable: isChecked, id: currentEditingAccessRule},
success: function(data){ success: function(data){
if (isChecked){ if (isChecked){
$("#ipTable").removeClass("disabled"); $("#ipTable").removeClass("disabled");
@ -867,10 +1022,10 @@
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "/api/blacklist/country/add", url: "/api/blacklist/country/add",
data: { cc: countryCode }, data: { cc: countryCode, id: currentEditingAccessRule},
success: function(response) { success: function(response) {
if (response.error != undefined){ if (response.error != undefined){
alert(response.error); msgbox(response.error, false);
} }
initBannedCountryList(); initBannedCountryList();
}, },
@ -886,10 +1041,10 @@
$.ajax({ $.ajax({
url: "/api/blacklist/country/remove", url: "/api/blacklist/country/remove",
method: "POST", method: "POST",
data: { cc: countryCode }, data: { cc: countryCode, id: currentEditingAccessRule},
success: function(response) { success: function(response) {
if (response.error != undefined){ if (response.error != undefined){
alert(response.error); msgbox(response.error, false);
} }
initBannedCountryList(); initBannedCountryList();
}, },
@ -916,7 +1071,7 @@
$.ajax({ $.ajax({
url: "/api/blacklist/ip/add", url: "/api/blacklist/ip/add",
type: "POST", type: "POST",
data: {ip: targetIp.toLowerCase()}, data: {ip: targetIp.toLowerCase(), id: currentEditingAccessRule},
success: function(response) { success: function(response) {
if (response.error !== undefined) { if (response.error !== undefined) {
msgbox(response.error, false, 6000); msgbox(response.error, false, 6000);
@ -938,7 +1093,7 @@
$.ajax({ $.ajax({
url: "/api/blacklist/ip/remove", url: "/api/blacklist/ip/remove",
type: "POST", type: "POST",
data: {ip: ipaddr.toLowerCase()}, data: {ip: ipaddr.toLowerCase(), id: currentEditingAccessRule},
success: function(response) { success: function(response) {
if (response.error !== undefined) { if (response.error !== undefined) {
msgbox(response.error, false, 6000); msgbox(response.error, false, 6000);
@ -962,7 +1117,7 @@
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: '/api/whitelist/enable', url: '/api/whitelist/enable',
data: { enable: isChecked }, data: { enable: isChecked , id: currentEditingAccessRule},
success: function(data){ success: function(data){
$(".toggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast"); $(".toggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
} }
@ -975,10 +1130,10 @@
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "/api/whitelist/country/add", url: "/api/whitelist/country/add",
data: { cc: countryCode }, data: { cc: countryCode , id: currentEditingAccessRule},
success: function(response) { success: function(response) {
if (response.error != undefined){ if (response.error != undefined){
alert(response.error); msgbox(response.error, false);
} }
initWhitelistCountryList(); initWhitelistCountryList();
}, },
@ -994,10 +1149,10 @@
$.ajax({ $.ajax({
url: "/api/whitelist/country/remove", url: "/api/whitelist/country/remove",
method: "POST", method: "POST",
data: { cc: countryCode }, data: { cc: countryCode , id: currentEditingAccessRule},
success: function(response) { success: function(response) {
if (response.error != undefined){ if (response.error != undefined){
alert(response.error); msgbox(response.error, false);
} }
initWhitelistCountryList(); initWhitelistCountryList();
}, },
@ -1025,7 +1180,7 @@
$.ajax({ $.ajax({
url: "/api/whitelist/ip/add", url: "/api/whitelist/ip/add",
type: "POST", type: "POST",
data: {ip: targetIp.toLowerCase(), "comment": remarks}, data: {ip: targetIp.toLowerCase(), "comment": remarks, id: currentEditingAccessRule},
success: function(response) { success: function(response) {
if (response.error !== undefined) { if (response.error !== undefined) {
msgbox(response.error, false, 6000); msgbox(response.error, false, 6000);
@ -1048,7 +1203,7 @@
$.ajax({ $.ajax({
url: "/api/whitelist/ip/remove", url: "/api/whitelist/ip/remove",
type: "POST", type: "POST",
data: {ip: ipaddr.toLowerCase()}, data: {ip: ipaddr.toLowerCase(), id: currentEditingAccessRule},
success: function(response) { success: function(response) {
if (response.error !== undefined) { if (response.error !== undefined) {
msgbox(response.error, false, 6000); msgbox(response.error, false, 6000);

View File

@ -237,7 +237,7 @@
msgbox("Certificate installed successfully"); msgbox("Certificate installed successfully");
if (callback != undefined){ if (callback != undefined){
callback(false); callback(true);
} }
} }
}, },

View File

@ -7,8 +7,12 @@
#httpProxyList .ui.toggle.checkbox input:checked ~ label::before{ #httpProxyList .ui.toggle.checkbox input:checked ~ label::before{
background-color: #00ca52 !important; background-color: #00ca52 !important;
} }
.subdEntry td:not(.ignoremw){
min-width: 200px;
}
</style> </style>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;"> <div style="width: 100%; overflow-x: auto; margin-bottom: 1em; min-height: 300px;">
<table class="ui celled sortable unstackable compact table"> <table class="ui celled sortable unstackable compact table">
<thead> <thead>
<tr> <tr>
@ -16,7 +20,7 @@
<th>Destination</th> <th>Destination</th>
<th>Virtual Directory</th> <th>Virtual Directory</th>
<th>Basic Auth</th> <th>Basic Auth</th>
<th class="no-sort" style="min-width:100px;">Actions</th> <th class="no-sort" style="min-width:150px;">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody id="httpProxyList"> <tbody id="httpProxyList">
@ -30,6 +34,8 @@
</div> </div>
<script> <script>
/* List all proxy endpoints */
function listProxyEndpoints(){ function listProxyEndpoints(){
$.get("/api/proxy/list?type=host", function(data){ $.get("/api/proxy/list?type=host", function(data){
$("#httpProxyList").html(``); $("#httpProxyList").html(``);
@ -42,6 +48,8 @@
<td data-label="" colspan="5"><i class="green check circle icon"></i> No HTTP Proxy Record</td> <td data-label="" colspan="5"><i class="green check circle icon"></i> No HTTP Proxy Record</td>
</tr>`); </tr>`);
}else{ }else{
//Sort by RootOrMatchingDomain field
data.sort((a,b) => (a.RootOrMatchingDomain > b.RootOrMatchingDomain) ? 1 : ((b.RootOrMatchingDomain > a.RootOrMatchingDomain) ? -1 : 0))
data.forEach(subd => { data.forEach(subd => {
let tlsIcon = ""; let tlsIcon = "";
let subdData = encodeURIComponent(JSON.stringify(subd)); let subdData = encodeURIComponent(JSON.stringify(subd));
@ -73,17 +81,33 @@
vdList = `<small style="opacity: 0.3; pointer-events: none; user-select: none;"><i class="check icon"></i> No Virtual Directory</small>`; vdList = `<small style="opacity: 0.3; pointer-events: none; user-select: none;"><i class="check icon"></i> No Virtual Directory</small>`;
} }
var enableChecked = "checked"; let enableChecked = "checked";
if (subd.Disabled){ if (subd.Disabled){
enableChecked = ""; enableChecked = "";
} }
let aliasDomains = ``;
if (subd.MatchingDomainAlias != undefined && subd.MatchingDomainAlias.length > 0){
aliasDomains = `<small class="aliasDomains" eptuuid="${subd.RootOrMatchingDomain}" style="color: #636363;">Alias: `;
subd.MatchingDomainAlias.forEach(alias => {
aliasDomains += `<a href="//${alias}" target="_blank">${alias}</a>, `;
});
aliasDomains = aliasDomains.substr(0, aliasDomains.length - 2); //Remove the last tailing seperator
aliasDomains += `</small><br>`;
}
$("#httpProxyList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry"> $("#httpProxyList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
<td data-label="" editable="true" datatype="inbound"><a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a> ${inboundTlsIcon}</td> <td data-label="" editable="true" datatype="inbound">
<a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a> ${inboundTlsIcon}<br>
${aliasDomains}
<small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small>
</td>
<td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</td> <td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</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="basicauth">${subd.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td> <td data-label="" editable="true" datatype="basicauth">
<td class="center aligned" editable="true" datatype="action" data-label=""> ${subd.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}
</td>
<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">
<input type="checkbox" class="enableToggle" name="active" ${enableChecked} eptuuid="${subd.RootOrMatchingDomain}" onchange="handleProxyRuleToggle(this);"> <input type="checkbox" class="enableToggle" name="active" ${enableChecked} eptuuid="${subd.RootOrMatchingDomain}" onchange="handleProxyRuleToggle(this);">
<label></label> <label></label>
@ -94,9 +118,87 @@
</tr>`); </tr>`);
}); });
} }
resolveAccessRuleNameOnHostRPlist();
}); });
} }
//Perform realtime alias update without refreshing the whole page
function updateAliasListForEndpoint(endpointName, newAliasDomainList){
let targetEle = $(`.aliasDomains[eptuuid='${endpointName}']`);
console.log(targetEle);
if (targetEle.length == 0){
return;
}
let aliasDomains = ``;
if (newAliasDomainList != undefined && newAliasDomainList.length > 0){
aliasDomains = `Alias: `;
newAliasDomainList.forEach(alias => {
aliasDomains += `<a href="//${alias}" target="_blank">${alias}</a>, `;
});
aliasDomains = aliasDomains.substr(0, aliasDomains.length - 2); //Remove the last tailing seperator
$(targetEle).html(aliasDomains);
$(targetEle).show();
}else{
$(targetEle).hide();
}
}
//Resolve & Update all rule names on host PR list
function resolveAccessRuleNameOnHostRPlist(){
//Resolve the access filters
$.get("/api/access/list", function(data){
console.log(data);
if (data.error == undefined){
//Build a map base on the data
let accessRuleMap = {};
for (var i = 0; i < data.length; i++){
accessRuleMap[data[i].ID] = data[i];
}
$(".accessRuleNameUnderHost").each(function(){
let thisAccessRuleID = $(this).attr("ruleid");
if (thisAccessRuleID== ""){
thisAccessRuleID = "default"
}
if (thisAccessRuleID == "default"){
//No need to label default access rules
$(this).html("");
return;
}
let rule = accessRuleMap[thisAccessRuleID];
let icon = `<i class="ui grey filter icon"></i>`;
if (rule.ID == "default"){
icon = `<i class="ui yellow star icon"></i>`;
}else if (rule.BlacklistEnabled && !rule.WhitelistEnabled){
//This is a blacklist filter
icon = `<i class="ui red filter icon"></i>`;
}else if (rule.WhitelistEnabled && !rule.BlacklistEnabled){
//This is a whitelist filter
icon = `<i class="ui green filter icon"></i>`;
}else if (rule.WhitelistEnabled && rule.BlacklistEnabled){
//Whitelist and blacklist filter
icon = `<i class="ui yellow filter icon"></i>`;
}
if (rule != undefined){
$(this).html(`${icon} ${rule.Name}`);
}
});
}
})
}
//Update the access rule name on given epuuid, call by hostAccessEditor.html
function updateAccessRuleNameUnderHost(epuuid, newruleUID){
$(`tr[eptuuid='${epuuid}'].subdEntry`).find(".accessRuleNameUnderHost").attr("ruleid", newruleUID);
resolveAccessRuleNameOnHostRPlist();
}
/* /*
Inline editor for httprp.html Inline editor for httprp.html
@ -191,6 +293,7 @@
<label>Skip WebSocket Origin Check<br> <label>Skip WebSocket Origin Check<br>
<small>Check this to allow cross-origin websocket requests</small></label> <small>Check this to allow cross-origin websocket requests</small></label>
</div> </div>
<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>
<!-- <button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editLoadBalanceOptions('${uuid}');"><i class="blue server icon"></i> Load Balance</button> --> <!-- <button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editLoadBalanceOptions('${uuid}');"><i class="blue server icon"></i> Load Balance</button> -->
</div> </div>
@ -213,7 +316,12 @@
<label>Allow plain HTTP access<br> <label>Allow plain HTTP access<br>
<small>Allow inbound connections without TLS/SSL</small></label> <small>Allow inbound connections without TLS/SSL</small></label>
</div><br> </div><br>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAliasHostnames('${uuid}');"><i class=" blue at icon"></i> Alias</button>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAccessRule('${uuid}');"><i class="ui filter icon"></i> Access Rule</button>
`); `);
$(".hostAccessRuleSelector").dropdown();
}else{ }else{
//Unknown field. Leave it untouched //Unknown field. Leave it untouched
} }
@ -277,6 +385,22 @@
showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload); showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload);
} }
function editAccessRule(uuid){
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
showSideWrapper("snippet/hostAccessEditor.html?t=" + Date.now() + "#" + payload);
}
function editAliasHostnames(uuid){
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
showSideWrapper("snippet/aliasEditor.html?t=" + Date.now() + "#" + payload);
}
function quickEditVdir(uuid){ function quickEditVdir(uuid){
openTabById("vdir"); openTabById("vdir");
$("#vdirBaseRoutingRule").parent().dropdown("set selected", uuid); $("#vdirBaseRoutingRule").parent().dropdown("set selected", uuid);
@ -314,6 +438,9 @@
}) })
} }
/* Access List handling */
//Bind on tab switch events //Bind on tab switch events
tabSwitchEventBind["httprp"] = function(){ tabSwitchEventBind["httprp"] = function(){

View File

@ -5,6 +5,12 @@
color: var(--theme_lgrey); color: var(--theme_lgrey);
border-radius: 1em !important; border-radius: 1em !important;
} }
.ui.form .sub.field{
background-color: var(--theme_advance);
border-radius: 0.6em;
padding: 1em;
}
</style> </style>
<div class="standardContainer"> <div class="standardContainer">
<div class="ui stackable grid"> <div class="ui stackable grid">
@ -16,7 +22,7 @@
<div class="field"> <div class="field">
<label>Matching Keyword / Domain</label> <label>Matching Keyword / Domain</label>
<input type="text" id="rootname" placeholder="mydomain.com"> <input type="text" id="rootname" placeholder="mydomain.com">
<small>Support subdomain and wildcard, e.g. s1.mydomain.com or *.test.mydomain.com</small> <small>Support subdomain and wildcard, e.g. s1.mydomain.com or *.test.mydomain.com. Use comma (,) for alias hostnames. </small>
</div> </div>
<div class="field"> <div class="field">
<label>Target IP Address or Domain Name with port</label> <label>Target IP Address or Domain Name with port</label>
@ -37,7 +43,18 @@
Advance Settings Advance Settings
</div> </div>
<div class="content"> <div class="content">
<p></p> <div class="field">
<label>Access Rule</label>
<div class="ui selection dropdown">
<input type="hidden" id="newProxyRuleAccessFilter" value="default">
<i class="dropdown icon"></i>
<div class="default text">Default</div>
<div class="menu" id="newProxyRuleAccessList">
<div class="item" data-value="default"><i class="ui yellow star icon"></i> Default</div>
</div>
</div>
<small>Allow regional access control using blacklist or whitelist. Use "default" for "allow all".</small>
</div>
<div class="field"> <div class="field">
<div class="ui checkbox"> <div class="ui checkbox">
<input type="checkbox" id="skipTLSValidation"> <input type="checkbox" id="skipTLSValidation">
@ -121,8 +138,6 @@
</div> </div>
</div> </div>
<script> <script>
$("#advanceProxyRules").accordion();
//New Proxy Endpoint //New Proxy Endpoint
function newProxyEndpoint(){ function newProxyEndpoint(){
@ -133,6 +148,7 @@
var bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked; var bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked;
var requireBasicAuth = $("#requireBasicAuth")[0].checked; var requireBasicAuth = $("#requireBasicAuth")[0].checked;
var skipWebSocketOriginCheck = $("#skipWebsocketOriginCheck")[0].checked; var skipWebSocketOriginCheck = $("#skipWebsocketOriginCheck")[0].checked;
var accessRuleToUse = $("#newProxyRuleAccessFilter").val();
if (rootname.trim() == ""){ if (rootname.trim() == ""){
$("#rootname").parent().addClass("error"); $("#rootname").parent().addClass("error");
@ -161,7 +177,7 @@
bypassGlobalTLS: bypassGlobalTLS, bypassGlobalTLS: bypassGlobalTLS,
bauth: requireBasicAuth, bauth: requireBasicAuth,
cred: JSON.stringify(credentials), cred: JSON.stringify(credentials),
access: accessRuleToUse,
}, },
success: function(data){ success: function(data){
if (data.error != undefined){ if (data.error != undefined){
@ -343,4 +359,47 @@
return back; return back;
} }
/*
Access Rule dropdown Initialization
*/
function initNewProxyRuleAccessDropdownList(callback=undefined){
$.get("/api/access/list", function(data){
if (data.error == undefined){
$("#newProxyRuleAccessList").html("");
data.forEach(function(rule){
let icon = `<i class="ui grey filter icon"></i>`;
if (rule.ID == "default"){
icon = `<i class="ui yellow star icon"></i>`;
}else if (rule.BlacklistEnabled && !rule.WhitelistEnabled){
//This is a blacklist filter
icon = `<i class="ui red filter icon"></i>`;
}else if (rule.WhitelistEnabled && !rule.BlacklistEnabled){
//This is a whitelist filter
icon = `<i class="ui green filter icon"></i>`;
}
$("#newProxyRuleAccessList").append(`<div class="item" data-value="${rule.ID}">${icon} ${rule.Name}</div>`);
});
$("#newProxyRuleAccessFilter").parent().dropdown();
if (callback != undefined){
callback();
}
}else{
msgbox("Access rule load failed: " + data.error, false);
}
})
}
initNewProxyRuleAccessDropdownList();
//Bind on tab switch events
tabSwitchEventBind["rules"] = function(){
//Update the access rule list
initNewProxyRuleAccessDropdownList();
}
$(document).ready(function(){
$("#advanceProxyRules").accordion();
$("#newProxyRuleAccessFilter").parent().dropdown();
});
</script> </script>

View File

@ -765,8 +765,11 @@
let data = Object.values(visitorData); let data = Object.values(visitorData);
Object.keys(visitorData).forEach(function(cc){ Object.keys(visitorData).forEach(function(cc){
console.log(cc);
if (cc == ""){ if (cc == ""){
labels.push("Local / Unknown") labels.push("Unknown")
}else if (cc == "lan"){
labels.push(`LAN / Loopback`);
}else{ }else{
labels.push(`${getCountryName(cc)} [${cc.toUpperCase()}]` ); labels.push(`${getCountryName(cc)} [${cc.toUpperCase()}]` );
} }

View File

@ -65,21 +65,9 @@
<div class="field"> <div class="field">
<p><i class="caret down icon"></i> Credentials for SMTP server authentications</p> <p><i class="caret down icon"></i> Credentials for SMTP server authentications</p>
<div class="two fields">
<div class="field"> <div class="field">
<label>Sender Username</label> <label>Sender Username / Email</label>
<input type="text" name="username" placeholder="E.g. admin"> <input type="text" name="username" placeholder="e.g. admin or admin@mydomain.com">
</div>
<div class="field">
<label>Sender Domain</label>
<div class="ui labeled input">
<div class="ui basic label">
@
</div>
<input type="text" name="domain" min="1" max="65534" placeholder="E.g. arozos.com">
</div>
</div>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
@ -272,7 +260,6 @@
e.preventDefault(); e.preventDefault();
var data = { var data = {
hostname: $('input[name=hostname]').val(), hostname: $('input[name=hostname]').val(),
domain: $('input[name=domain]').val(),
port: parseInt($('input[name=port]').val()), port: parseInt($('input[name=port]').val()),
username: $('input[name=username]').val(), username: $('input[name=username]').val(),
password: $('input[name=password]').val(), password: $('input[name=password]').val(),
@ -306,7 +293,6 @@
function initSMTPSettings(){ function initSMTPSettings(){
$.get("/api/tools/smtp/get", function(data){ $.get("/api/tools/smtp/get", function(data){
$('#email-form input[name=hostname]').val(data.Hostname); $('#email-form input[name=hostname]').val(data.Hostname);
$('#email-form input[name=domain]').val(data.Domain);
$('#email-form input[name=port]').val(data.Port); $('#email-form input[name=port]').val(data.Port);
$('#email-form input[name=username]').val(data.Username); $('#email-form input[name=username]').val(data.Username);
$('#email-form input[name=senderAddr]').val(data.SenderAddr); $('#email-form input[name=senderAddr]').val(data.SenderAddr);
@ -345,14 +331,6 @@
form.find('input[name="hostname"]').parent().removeClass('error'); form.find('input[name="hostname"]').parent().removeClass('error');
} }
// validate domain
const domain = form.find('input[name="domain"]').val().trim();
if (!domainRegex.test(domain)) {
form.find('input[name="domain"]').parent().addClass('error');
isValid = false;
} else {
form.find('input[name="domain"]').parent().removeClass('error');
}
// validate username // validate username
const username = form.find('input[name="username"]').val().trim(); const username = form.find('input[name="username"]').val().trim();

View File

@ -2,9 +2,6 @@
index.html style overwrite index.html style overwrite
*/ */
:root{ :root{
--theme_background: linear-gradient(60deg, rgb(84, 58, 183) 0%, rgb(0, 172, 193) 100%); --theme_background: linear-gradient(60deg, rgb(84, 58, 183) 0%, rgb(0, 172, 193) 100%);
--theme_background_inverted: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%); --theme_background_inverted: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
--theme_green: linear-gradient(270deg, #27e7ff, #00ca52); --theme_green: linear-gradient(270deg, #27e7ff, #00ca52);
@ -256,7 +253,7 @@ body{
.sideWrapperMenu{ .sideWrapperMenu{
height: 3px; height: 3px;
background-color: #414141; background: var(--theme_background);
} }
/* /*

View File

@ -51,7 +51,7 @@
font-family: 'Lato'; font-family: 'Lato';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: local('Lato Bold'), local('Lato-Bold'), url(https://fonts.gstatic.com/s/lato/v17/S6u9w4BMUTPHh6UVSwaPGR_p.woff2) format('woff2'); src: local('Lato Bold'), local('Lato-Bold'), url(fonts/S6u9w4BMUTPHh6UVSwaPGR_p.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
} }
/* latin */ /* latin */
@ -59,6 +59,6 @@
font-family: 'Lato'; font-family: 'Lato';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: local('Lato Bold'), local('Lato-Bold'), url(https://fonts.gstatic.com/s/lato/v17/S6u9w4BMUTPHh6UVSwiPGQ.woff2) format('woff2'); src: local('Lato Bold'), local('Lato-Bold'), url(fonts/S6u9w4BMUTPHh6UVSwiPGQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }

View File

@ -0,0 +1,267 @@
<!DOCTYPE html>
<html>
<head>
<!-- Notes: This should be open in its original path-->
<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>
<style>
#refreshAccessRuleListBtn{
position: absolute;
top: 0.4em;
right: 1em;
}
</style>
</head>
<body>
<br>
<div class="ui container">
<div class="ui header">
<div class="content">
Access Rule Editor
<div class="sub header">Create, Edit or Remove Access Rules</div>
</div>
</div>
<div class="ui divider"></div>
<div class="ui top attached tabular menu">
<a class="active item" data-tab="new"><i class="ui green add icon"></i> New</a>
<a class="item" data-tab="edit"><i class="ui grey edit icon"></i> Edit</a>
</div>
<div class="ui bottom attached active tab segment" data-tab="new">
<p>Create a new Access Rule</p>
<form class="ui form" id="accessRuleForm">
<div class="field">
<label>Rule Name</label>
<input type="text" name="accessRuleName" placeholder="Rule Name" required>
</div>
<div class="field">
<label>Description</label>
<textarea name="description" placeholder="Description" required></textarea>
</div>
<button class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
</form>
<br>
</div>
<div class="ui bottom attached tab segment" data-tab="edit">
<p>Select an Access Rule to edit</p>
<button id="refreshAccessRuleListBtn" class="ui circular basic icon button" onclick="reloadAccessRuleList()"><i class="ui green refresh icon"></i></button>
<div class="ui selection fluid dropdown" id="accessRuleSelector">
<input type="hidden" name="targetAccessRule" value="default">
<i class="dropdown icon"></i>
<div class="default text"></div>
<div class="menu" id="accessRuleList">
<div class="item" data-value="default"><i class="ui yellow star icon"></i> Default</div>
</div>
</div>
<br>
<form class="ui form" id="modifyRuleInfo">
<div class="disabled field">
<label>Rule ID</label>
<input type="text" name="accessRuleUUID">
</div>
<div class="field">
<label>Rule Name</label>
<input type="text" name="accessRuleName" placeholder="Rule Name" required>
</div>
<div class="field">
<label>Description</label>
<textarea name="description" placeholder="Description" required></textarea>
</div>
<button class="ui basic button" type="submit"><i class="ui green save icon"></i> Save Changes</button>
<button class="ui basic button" onclick="removeAccessRule(event);"><i class="ui red trash icon"></i> Remove Rule</button>
</form>
</div>
<br>
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Close</button>
<br><br><br>
</div>
<script>
let accessRuleList = [];
$('.dropdown').dropdown();
$('.menu .item').tab();
function handleCreateNewAccessRule(event) {
event.preventDefault(); // Prevent the default form submission
const formData = new FormData(event.target);
const accessRuleName = formData.get('accessRuleName');
const description = formData.get('description');
console.log('Access Rule Name:', accessRuleName);
console.log('Description:', description);
$("#accessRuleForm input[name='accessRuleName']").val("");
$("#accessRuleForm textarea[name='description']").val("");
$.ajax({
url: "/api/access/create",
method: "POST",
data: {
"name": accessRuleName,
"desc": description
},
success: function(data){
if (data.error != undefined){
parent.msgbox(data.error, false);
}else{
parent.msgbox("Access Rule Created", true);
reloadAccessRuleList();
if (parent != undefined && parent.reloadAccessRules != undefined){
parent.reloadAccessRules();
}
}
}
})
}
//Handle on change of the dropdown selection
function handleSelectEditingAccessRule(){
const selectedValue = document.querySelector('#accessRuleSelector').querySelector('input').value;
console.log('Selected Value:', selectedValue);
//Load the information from list
loadAccessRuleInfoIntoEditFields(selectedValue);
}
//Load the access rules information into the fields
function loadAccessRuleInfoIntoEditFields(targetAccessRuleUUID){
var targetAccessRule = undefined;
for (var i = 0; i < accessRuleList.length; i++){
let thisAccessRule = accessRuleList[i];
if (thisAccessRule.ID == targetAccessRuleUUID){
targetAccessRule = thisAccessRule;
}
}
if (targetAccessRule == undefined){
//Target exists rule no longer exists
return;
}
let accessRuleID = targetAccessRule.ID;
let accessRuleName = targetAccessRule.Name;
let accessRuleDesc = targetAccessRule.Desc;
//Load the information into the form input field
//Load the information into the form input field
document.querySelector('#modifyRuleInfo input[name="accessRuleUUID"]').value = accessRuleID;
document.querySelector('#modifyRuleInfo input[name="accessRuleName"]').value = accessRuleName;
document.querySelector('#modifyRuleInfo textarea[name="description"]').value = accessRuleDesc;
}
//Bind events to modify rule form
document.getElementById('modifyRuleInfo').addEventListener('submit', function(event){
event.preventDefault(); // Prevent the default form submission
const accessRuleUUID = document.querySelector('#modifyRuleInfo input[name="accessRuleUUID"]').value;
const accessRuleName = document.querySelector('#modifyRuleInfo input[name="accessRuleName"]').value;
const description = document.querySelector('#modifyRuleInfo textarea[name="description"]').value;
console.log('Access Rule UUID:', accessRuleUUID);
console.log('Access Rule Name:', accessRuleName);
console.log('Description:', description);
$.ajax({
url: "/api/access/update",
method: "POST",
data: {
"id":accessRuleUUID,
"name":accessRuleName,
"desc":description
},
success: function(data){
if (data.error != undefined){
parent.msgbox(data.error, false);
}else{
parent.msgbox("Access rule updated", true);
initAccessRuleList(function(){
$("#accessRuleSelector").dropdown("set selected", accessRuleUUID);
loadAccessRuleInfoIntoEditFields(accessRuleUUID);
});
if (parent != undefined && parent.reloadAccessRules != undefined){
parent.reloadAccessRules();
}
}
}
})
});
function initAccessRuleList(callback=undefined){
$.get("/api/access/list", function(data){
if (data.error == undefined){
$("#accessRuleList").html("");
data.forEach(function(rule){
let icon = `<i class="ui grey filter icon"></i>`;
if (rule.ID == "default"){
icon = `<i class="ui yellow star icon"></i>`;
}else if (rule.BlacklistEnabled && !rule.WhitelistEnabled){
//This is a blacklist filter
icon = `<i class="ui red filter icon"></i>`;
}else if (rule.WhitelistEnabled && !rule.BlacklistEnabled){
//This is a whitelist filter
icon = `<i class="ui green filter icon"></i>`;
}
$("#accessRuleList").append(`<div class="item" data-value="${rule.ID}">${icon} ${rule.Name}</div>`);
});
accessRuleList = data;
$(".dropdown").dropdown();
if (callback != undefined){
callback();
}
}
})
}
initAccessRuleList(function(){
$("#accessRuleSelector").dropdown("set selected", "default");
loadAccessRuleInfoIntoEditFields("default");
});
function reloadAccessRuleList(){
initAccessRuleList(function(){
$("#accessRuleSelector").dropdown("set selected", "default");
loadAccessRuleInfoIntoEditFields("default");
});
}
function removeAccessRule(event){
event.preventDefault();
event.stopImmediatePropagation();
let accessRuleUUID = $("#modifyRuleInfo input[name='accessRuleUUID']").val();
if (accessRuleUUID == ""){
return;
}
if (accessRuleUUID == "default"){
parent.msgbox("Default access rule cannot be removed", false);
return;
}
let accessRuleName = $("#modifyRuleInfo input[name='accessRuleName']").val();
if (confirm("Confirm removing access rule " + accessRuleName + "?")){
$.ajax({
url: "/api/access/remove",
data: {
"id": accessRuleUUID
},
method: "POST",
success: function(data){
if (data.error != undefined){
parent.msgbox(data.error, false);
}else{
parent.msgbox("Access rule removed", true);
reloadAccessRuleList();
if (parent != undefined && parent.reloadAccessRules != undefined){
parent.reloadAccessRules();
}
}
}
})
}
}
document.getElementById('accessRuleSelector').addEventListener('change', handleSelectEditingAccessRule);
document.getElementById('accessRuleForm').addEventListener('submit', handleCreateNewAccessRule);
</script>
</body>
</html>

View File

@ -118,6 +118,14 @@
<label>ACME Server URL</label> <label>ACME Server URL</label>
<input id="caURL" type="text" placeholder="https://example.com/acme/dictionary"> <input id="caURL" type="text" placeholder="https://example.com/acme/dictionary">
</div> </div>
<div class="field" id="kidInput" style="display:none;">
<label>EAB Credentials (KID) for current provider</label>
<input id="eab_kid" type="text" placeholder="Leave this field blank to keep the current configuration">
</div>
<div class="field" id="hmacInput" style="display:none;">
<label>EAB HMAC Key for current provider</label>
<input id="eab_hmac" type="text" placeholder="Leave this field blank to keep the current configuration">
</div>
<div class="field" id="skipTLS" style="display:none;"> <div class="field" id="skipTLS" style="display:none;">
<div class="ui checkbox"> <div class="ui checkbox">
<input type="checkbox" id="skipTLSCheckbox"> <input type="checkbox" id="skipTLSCheckbox">
@ -314,19 +322,88 @@
// Button click event handler for obtaining certificate // Button click event handler for obtaining certificate
$("#obtainButton").click(function() { $("#obtainButton").click(function() {
$("#obtainButton").addClass("loading").addClass("disabled"); $("#obtainButton").addClass("loading").addClass("disabled");
updateCertificateEAB();
obtainCertificate(); obtainCertificate();
}); });
$("input[name=ca]").on('change', function() { $("input[name=ca]").on('change', function() {
if(this.value == "Custom ACME Server") { if(this.value == "Custom ACME Server") {
$("#caInput").show(); $("#caInput").show();
$("#kidInput").show();
$("#hmacInput").show();
$("#skipTLS").show(); $("#skipTLS").show();
} else { } else if (this.value == "ZeroSSL") {
$("#kidInput").show();
$("#hmacInput").show();
} else if (this.value == "Buypass") {
$("#kidInput").show();
$("#hmacInput").show();
}else {
$("#caInput").hide(); $("#caInput").hide();
$("#skipTLS").hide(); $("#skipTLS").hide();
$("#kidInput").hide();
$("#hmacInput").hide();
} }
}) })
// Obtain certificate from API
function updateCertificateEAB() {
var ca = $("#ca").dropdown("get value");
var caURL = "";
if (ca == "Custom ACME Server") {
ca = "custom";
caURL = $("#caURL").val();
}else if(ca == "Buypass") {
caURL = "https://api.buypass.com/acme/directory";
}else if(ca == "ZeroSSL") {
caURL = "https://acme.zerossl.com/v2/DV90";
}
if(caURL == "") {
return;
}
var kid = $("#eab_kid").val();
var hmac = $("#eab_hmac").val();
if(kid == "" || hmac == "") {
return;
}
console.log(caURL + " " + kid + " " + hmac);
$.ajax({
url: "/api/acme/autoRenew/setEAB",
method: "GET",
data: {
acmeDirectoryURL: caURL,
kid: kid,
hmacEncoded: hmac,
},
success: function(response) {
//$("#obtainButton").removeClass("loading").removeClass("disabled");
if (response.error) {
console.log("Error:", response.error);
// Show error message
parent.msgbox(response.error, false, 12000);
} else {
console.log("Certificate EAB updated successfully");
// Show success message
parent.msgbox("Certificate EAB updated successfully");
// Renew the parent certificate list
parent.initManagedDomainCertificateList();
}
},
error: function(error) {
//$("#obtainButton").removeClass("loading").removeClass("disabled");
console.log("Failed to update EAB configuration:", error);
parent.msgbox("Failed to update EAB configuration");
}
});
}
// Obtain certificate from API // Obtain certificate from API
function obtainCertificate() { function obtainCertificate() {
var domains = $("#domainsInput").val(); var domains = $("#domainsInput").val();

View File

@ -0,0 +1,178 @@
<!DOCTYPE html>
<html>
<head>
<!-- Notes: This should be open in its original path-->
<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>
</head>
<body>
<br>
<div class="ui container">
<div class="ui header">
<div class="content">
Alias Hostname
<div class="sub header epname"></div>
</div>
</div>
<div class="ui divider"></div>
<div class="scrolling content ui form">
<div id="inlineEditBasicAuthCredentials" class="field">
<p>Enter alias hostname or wildcard matching keywords for <code class="epname"></code></p>
<table class="ui very basic compacted unstackable celled table">
<thead>
<tr>
<th>Alias Hostname</th>
<th>Remove</th>
</tr></thead>
<tbody id="inlineEditTable">
<tr>
<td colspan="2"><i class="ui green circle check icon"></i> No Alias Hostname</td>
</tr>
</tbody>
</table>
<div class="ui divider"></div>
<div class="three small fields">
<div class="field">
<label>Alias Hostname</label>
<input id="aliasHostname" type="text" placeholder="alias.mydomain.com" autocomplete="off">
<small>Support wildcards e.g. alias.mydomain.com or *.alias.mydomain.com</small>
</div>
<div class="field" >
<button class="ui basic button" onclick="addAliasToRoutingRule();"><i class="green add icon"></i> Add Alias</button>
</div>
<div class="ui divider"></div>
</div>
</div>
</div>
<div class="ui divider"></div>
<div class="field" >
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
</div>
</div>
<br><br><br><br>
</div>
<script>
let aliasList = [];
let editingEndpoint = {};
if (window.location.hash.length > 1){
let payloadHash = window.location.hash.substr(1);
try{
payloadHash = JSON.parse(decodeURIComponent(payloadHash));
$(".epname").text(payloadHash.ep);
editingEndpoint = payloadHash;
}catch(ex){
console.log("Unable to load endpoint data from hash")
}
}
function initAliasNames(){
$.ajax({
url: "/api/proxy/detail",
method: "POST",
data: {
"type":"host",
"epname": editingEndpoint.ep
},
success: function(data){
if (data.error != undefined){
//This endpoint not exists?
alert(data.error);
return;
}else{
$("#inlineEditTable").html("");
if (data.MatchingDomainAlias != undefined){
aliasList = data.MatchingDomainAlias;
renderAliasList();
}else{
//Assume no alias
$("#inlineEditTable").html(`<tr>
<td colspan="2"><i class="ui green circle check icon"></i> No Alias Hostname</td>
</tr>`);
}
}
}
})
}
initAliasNames();
function removeAliasDomain(targetDomain){
aliasList.splice(aliasList.indexOf(targetDomain), 1);
saveCurrentAliasList(function(data){
if (data.error != undefined){
parent.msgbox(data.error);
}else{
initAliasNames();
parent.msgbox("Alias hostname removed")
}
});
}
function addAliasToRoutingRule(){
let newAliasHostname = $("#aliasHostname").val().trim();
aliasList.push(newAliasHostname);
$("#aliasHostname").val("");
saveCurrentAliasList(function(data){
if (data.error != undefined){
parent.msgbox(data.error);
}else{
parent.msgbox("New alias hostname added")
initAliasNames();
}
});
}
function saveCurrentAliasList(callback=undefined){
$.ajax({
url: "/api/proxy/setAlias",
method: "POST",
data:{
"ep":editingEndpoint.ep,
"alias": JSON.stringify(aliasList)
},
success: function(data){
if (callback != undefined){
callback(data);
}
if (data.error == undefined && parent != undefined && parent.document != undefined){
//Try to update the parent object's rules if exists
parent.updateAliasListForEndpoint(editingEndpoint.ep, aliasList);
}
}
})
}
function renderAliasList(){
$("#inlineEditTable").html("");
aliasList.forEach(aliasDomain => {
let domainLink = `<a href="//${aliasDomain}" target="_blank">${aliasDomain}</a>`
if (aliasDomain.includes("*")){
//This is a wildcard hostname
domainLink = aliasDomain;
}
$("#inlineEditTable").append(`<tr>
<td>${domainLink}</td>
<td><button class="ui basic button" onclick="removeAliasDomain('${aliasDomain}');"><i class="red remove icon"></i> Remove</button></td>
</tr>`);
});
if (aliasList.length == 0){
$("#inlineEditTable").html(`<tr>
<td colspan="2"><i class="ui green circle check icon"></i> No Alias Hostname</td>
</tr>`);
}
}
function closeThisWrapper(){
parent.hideSideWrapper(true);
}
</script>
</body>
</html>

View File

@ -0,0 +1,187 @@
<!DOCTYPE html>
<html>
<head>
<!-- Notes: This should be open in its original path-->
<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>
<style>
.accessRule{
cursor: pointer;
border-radius: 0.4em !important;
border: 1px solid rgb(233, 233, 233) !important;
}
.accessRule:hover{
background-color: rgb(241, 241, 241) !important;
}
.accessRule.active{
background-color: rgb(241, 241, 241) !important;
}
.accessRule .selected{
position: absolute;
top: 1em;
right: 0.6em;
}
.accessRule:not(.active) .selected{
display:none;
}
#accessRuleList{
padding: 0.6em;
border: 1px solid rgb(228, 228, 228);
border-radius: 0.4em !important;
max-height: calc(100vh - 15em);
min-height: 300px;
overflow-y: auto;
}
</style>
</head>
<body>
<br>
<div class="ui container">
<div class="ui header">
<div class="content">
Host Access Settings
<div class="sub header" id="epname"></div>
</div>
</div>
<div class="ui divider"></div>
<p>Select an access rule to apply blacklist / whitelist filtering</p>
<div id="accessRuleList">
<div class="ui segment accessRule">
<div class="ui header">
<i class="filter icon"></i>
<div class="content">
Account Settings
<div class="sub header">Manage your preferences</div>
</div>
</div>
</div>
</div>
<br>
<button class="ui basic button" onclick="applyChangeAndClose()"><i class="ui green check icon"></i> Apply Change</button>
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Close</button>
<br><br><br>
</div>
<script>
let editingEndpoint = {};
if (window.location.hash.length > 1){
let payloadHash = window.location.hash.substr(1);
try{
payloadHash = JSON.parse(decodeURIComponent(payloadHash));
$("#epname").text(payloadHash.ep);
editingEndpoint = payloadHash;
}catch(ex){
console.log("Unable to load endpoint data from hash")
}
}
function initAccessRuleList(callback = undefined){
$("#accessRuleList").html("<small>Loading</small>");
$.get("/api/access/list", function(data){
if (data.error == undefined){
$("#accessRuleList").html("");
data.forEach(function(rule){
let icon = `<i class="ui grey filter icon"></i>`;
if (rule.ID == "default"){
icon = `<i class="ui yellow star icon"></i>`;
}else if (rule.BlacklistEnabled && !rule.WhitelistEnabled){
//This is a blacklist filter
icon = `<i class="ui red filter icon"></i>`;
}else if (rule.WhitelistEnabled && !rule.BlacklistEnabled){
//This is a whitelist filter
icon = `<i class="ui green filter icon"></i>`;
}else if (rule.WhitelistEnabled && rule.BlacklistEnabled){
//Whitelist and blacklist filter
icon = `<i class="ui yellow filter icon"></i>`;
}
$("#accessRuleList").append(`<div class="ui basic segment accessRule" ruleid="${rule.ID}" onclick="selectThisRule(this);">
<h5 class="ui header">
${icon}
<div class="content">
${rule.Name}
<div class="sub header">${rule.ID}</div>
</div>
</h5>
<p>${rule.Desc}</p>
${rule.BlacklistEnabled?`<small><i class="ui red filter icon"></i> Blacklist Enabled</small>`:""}
${rule.WhitelistEnabled?`<small><i class="ui green filter icon"></i> Whitelist Enabled</small>`:""}
<div class="selected"><i class="ui large green check icon"></i></div>
</div>`);
});
accessRuleList = data;
$(".dropdown").dropdown();
if (callback != undefined){
callback();
}
}
});
}
initAccessRuleList(function(){
$.ajax({
url: "/api/proxy/detail",
method: "POST",
data: {"type":"host", "epname": editingEndpoint.ep },
success: function(data){
console.log(data);
if (data.error != undefined){
alert(data.error);
}else{
let currentAccessFilter = data.AccessFilterUUID;
if (currentAccessFilter == ""){
//Use default
currentAccessFilter = "default";
}
$(`.accessRule[ruleid=${currentAccessFilter}]`).addClass("active");
}
}
})
});
function selectThisRule(accessRuleObject){
let accessRuleID = $(accessRuleObject).attr("ruleid");
$(".accessRule").removeClass('active');
$(accessRuleObject).addClass('active');
}
function applyChangeAndClose(){
let newAccessRuleID = $(".accessRule.active").attr("ruleid");
let targetEndpoint = editingEndpoint.ep;
$.ajax({
url: "/api/access/attach",
method: "POST",
data: {
id: newAccessRuleID,
host: targetEndpoint
},
success: function(data){
if (data.error != undefined){
parent.msgbox(data.error, false);
}else{
parent.msgbox("Access Rule Updated");
//Modify the parent list if exists
if (parent != undefined && parent.updateAccessRuleNameUnderHost){
parent.updateAccessRuleNameUnderHost(targetEndpoint, newAccessRuleID);
}
parent.hideSideWrapper();
}
}
})
}
</script>
</body>
</html>