13 Commits
3.0.2 ... 3.0.3

Author SHA1 Message Date
Toby Chui
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
Toby Chui
e2a449a7bc Update blacklist.go
Fixed blacklist CIDR not working bug
2024-04-30 13:39:48 +08:00
Toby Chui
a9695e969e Update Server.go
Fixed default site bypassing access filter bug
2024-04-30 13:25:26 +08:00
Toby Chui
7ba997dfc2 Added support for changing mdns name
+ Added `mdnsname` startup flag
2024-04-30 11:49:34 +08:00
Toby Chui
d00117e878 Merge pull request #135 from PassiveLemon/Graceful-Shutdown
Fix: Graceful container shutdown
2024-04-29 09:18:03 +08:00
PassiveLemon
43a84a3f1c Fix: Graceful container shutdown 2024-04-28 10:55:45 -04:00
Toby Chui
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
Toby Chui
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
Toby Chui
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
Toby Chui
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
Toby Chui
73276b1918 Update README.md
Fixed description that easily cause misunderstanding
2024-04-26 22:36:12 +08:00
Toby Chui
abdb7d4d75 Merge pull request #125 from Morethanevil/main
Update CHANGELOG.md
2024-04-24 23:55:23 +08:00
Marcel
72299ace15 Update CHANGELOG.md 2024-04-24 17:51:36 +02:00
18 changed files with 100 additions and 64 deletions

View File

@@ -1,3 +1,13 @@
# 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 # v3.0.1 Apr 04 2024
## Bugfixupdate for big release of V3, read update notes from V3 if you are still on V2 ## Bugfixupdate for big release of V3, read update notes from V3 if you are still on V2

View File

@@ -18,7 +18,7 @@ General purpose request (reverse) proxy and forwarding tool for networking noobs
- 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

@@ -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

@@ -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

@@ -41,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)")
@@ -51,7 +52,7 @@ var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
var ( var (
name = "Zoraxy" name = "Zoraxy"
version = "3.0.2" 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()

View File

@@ -69,6 +69,9 @@ func NewAccessController(options *Options) (*Controller, error) {
Options: options, Options: options,
} }
//Assign default access rule parent
thisController.DefaultAccessRule.parent = &thisController
//Load all acccess rules from file //Load all acccess rules from file
configFiles, err := filepath.Glob(options.ConfigFolder + "/*.json") configFiles, err := filepath.Glob(options.ConfigFolder + "/*.json")
if err != nil { if err != nil {
@@ -113,6 +116,7 @@ func (c *Controller) GetGlobalAccessRule() (*AccessRule, error) {
// Load access rules to runtime, require rule ID // Load access rules to runtime, require rule ID
func (c *Controller) GetAccessRuleByID(accessRuleID string) (*AccessRule, error) { func (c *Controller) GetAccessRuleByID(accessRuleID string) (*AccessRule, error) {
if accessRuleID == "default" || accessRuleID == "" { if accessRuleID == "default" || accessRuleID == "" {
return c.DefaultAccessRule, nil return c.DefaultAccessRule, nil
} }
//Load from sync.Map, should be O(1) //Load from sync.Map, should be O(1)

View File

@@ -2,6 +2,8 @@ package access
import ( import (
"strings" "strings"
"imuslab.com/zoraxy/mod/netutils"
) )
/* /*
@@ -71,5 +73,22 @@ func (s *AccessRule) GetAllBlacklistedIp() []string {
func (s *AccessRule) IsIPBlacklisted(ipAddr string) bool { func (s *AccessRule) IsIPBlacklisted(ipAddr string) bool {
IPBlacklist := *s.BlackListIP IPBlacklist := *s.BlackListIP
_, ok := IPBlacklist[ipAddr] _, ok := IPBlacklist[ipAddr]
return ok 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
} }

View File

@@ -87,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
@@ -102,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, "/") {

View File

@@ -21,6 +21,7 @@ func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter,
w.Write([]byte("500 - Internal Server Error")) w.Write([]byte("500 - Internal Server Error"))
return true return true
} }
isBlocked, blockedReason := accessRequestBlocked(accessRule, h.Parent.Option.WebDirectory, w, r) isBlocked, blockedReason := accessRequestBlocked(accessRule, h.Parent.Option.WebDirectory, w, r)
if isBlocked { if isBlocked {
h.logRequest(r, false, 403, blockedReason, "") h.logRequest(r, false, 403, blockedReason, "")

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

@@ -13,7 +13,6 @@ 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
@@ -21,10 +20,9 @@ type Sender struct {
} }
// 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,
@@ -36,9 +34,11 @@ 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 {
@@ -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

@@ -83,6 +83,11 @@ func (s *Store) GetRequesterCountryISOCode(r *http.Request) string {
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

@@ -93,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

@@ -160,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",

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();