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
This commit is contained in:
Toby Chui 2024-04-30 14:39:49 +08:00 committed by GitHub
commit 176249a7d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 85 additions and 52 deletions

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,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,
"Free donuts", test@example.com,
"Come get your free donuts on this Sunday!" "Free donuts",
) "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

@ -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 / Email</label>
<label>Sender Username</label> <input type="text" name="username" placeholder="e.g. admin or admin@mydomain.com">
<input type="text" name="username" placeholder="E.g. admin">
</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();