Updated a lot of stuffs

+ Added comments for whitelist
+ Added automatic cert pick for multi-host certs (SNI)
+ Renamed .crt to .pem for cert store
+ Added best-fit selection for wildcard matching rules
+ Added x-proxy-by header
+ Added X-real-Ip header
+ Added Development Mode (Cache-Control: no-store)
+ Updated utm timeout to 10 seconds instead of 90
This commit is contained in:
Toby Chui 2024-02-16 15:44:09 +08:00
parent 174efc9080
commit e980bc847b
41 changed files with 1056 additions and 531 deletions

View File

@ -4,6 +4,8 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
strip "github.com/grokify/html-strip-tags-go"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
@ -115,7 +117,7 @@ func handleListWhitelisted(w http.ResponseWriter, r *http.Request) {
bltype = "country" bltype = "country"
} }
resulst := []string{} resulst := []*geodb.WhitelistEntry{}
if bltype == "country" { if bltype == "country" {
resulst = geodbStore.GetAllWhitelistedCountryCode() resulst = geodbStore.GetAllWhitelistedCountryCode()
} else if bltype == "ip" { } else if bltype == "ip" {
@ -134,7 +136,10 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.AddCountryCodeToWhitelist(countryCode) comment, _ := utils.PostPara(r, "comment")
comment = strip.StripTags(comment)
geodbStore.AddCountryCodeToWhitelist(countryCode, comment)
utils.SendOK(w) utils.SendOK(w)
} }
@ -158,7 +163,10 @@ func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.AddIPToWhiteList(ipAddr) comment, _ := utils.PostPara(r, "comment")
comment = strip.StripTags(comment)
geodbStore.AddIPToWhiteList(ipAddr, comment)
} }
func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) { func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {

View File

@ -56,6 +56,7 @@ func initAPIs() {
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect) authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener) authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck) authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
//Reverse proxy virtual directory APIs //Reverse proxy virtual directory APIs
authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir) authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir) authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
@ -178,7 +179,7 @@ func initAPIs() {
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus) authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer) authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer) authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
authRouter.HandleFunc("/api/webserv/setPort", staticWebServer.HandlePortChange) authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing) authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
if *allowWebFileManager { if *allowWebFileManager {
//Web Directory Manager file operation functions //Web Directory Manager file operation functions

View File

@ -51,7 +51,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
results := []*CertInfo{} results := []*CertInfo{}
for _, filename := range filenames { for _, filename := range filenames {
certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".crt") certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".pem")
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key") //keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
fileInfo, err := os.Stat(certFilepath) fileInfo, err := os.Stat(certFilepath)
if err != nil { if err != nil {
@ -248,7 +248,7 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
} }
if keytype == "pub" { if keytype == "pub" {
overWriteFilename = domain + ".crt" overWriteFilename = domain + ".pem"
} else if keytype == "pri" { } else if keytype == "pri" {
overWriteFilename = domain + ".key" overWriteFilename = domain + ".key"
} else { } else {
@ -287,6 +287,9 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
return return
} }
//Update cert list
tlsCertManager.UpdateLoadedCertList()
// send response // send response
fmt.Fprintln(w, "File upload successful!") fmt.Fprintln(w, "File upload successful!")
} }

View File

@ -10,6 +10,7 @@ require (
github.com/gorilla/sessions v1.2.1 github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/grandcat/zeroconf v1.0.0 github.com/grandcat/zeroconf v1.0.0
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
github.com/likexian/whois v1.15.1 github.com/likexian/whois v1.15.1
github.com/microcosm-cc/bluemonday v1.0.25 github.com/microcosm-cc/bluemonday v1.0.25
golang.org/x/net v0.14.0 golang.org/x/net v0.14.0

View File

@ -740,6 +740,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE= github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=

View File

@ -163,7 +163,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
// Each certificate comes back with the cert bytes, the bytes of the client's // Each certificate comes back with the cert bytes, the bytes of the client's
// private key, and a certificate URL. // private key, and a certificate URL.
err = os.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777) err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return false, err return false, err

View File

@ -1,8 +1,6 @@
package dynamicproxy package dynamicproxy
import ( import (
_ "embed"
"errors"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -25,11 +23,6 @@ import (
- Vitrual Directory Routing - Vitrual Directory Routing
*/ */
var (
//go:embed tld.json
rawTldMap []byte
)
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/* /*
Special Routing Rules, bypass most of the limitations Special Routing Rules, bypass most of the limitations
@ -52,10 +45,12 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
//Inject debug headers
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
/* /*
General Access Check General Access Check
*/ */
respWritten := h.handleAccessRouting(w, r) respWritten := h.handleAccessRouting(w, r)
if respWritten { if respWritten {
return return
@ -81,6 +76,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/* /*
Host Routing Host Routing
*/ */
sep := h.Parent.getProxyEndpointFromHostname(domainOnly) sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
if sep != nil && !sep.Disabled { if sep != nil && !sep.Disabled {
if sep.RequireBasicAuth { if sep.RequireBasicAuth {
@ -235,44 +231,3 @@ func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Reques
return false return false
} }
// Return if the given host is already topped (e.g. example.com or example.co.uk) instead of
// a host with subdomain (e.g. test.example.com)
func (h *ProxyHandler) isTopLevelRedirectableDomain(requestHost string) bool {
parts := strings.Split(requestHost, ".")
if len(parts) > 2 {
//Cases where strange tld is used like .co.uk or .com.hk
_, ok := h.Parent.tldMap[strings.Join(parts[1:], ".")]
if ok {
//Already topped
return true
}
} else {
//Already topped
return true
}
return false
}
// GetTopLevelRedirectableDomain returns the toppest level of domain
// that is redirectable. E.g. a.b.c.example.co.uk will return example.co.uk
func (h *ProxyHandler) getTopLevelRedirectableDomain(unsetSubdomainHost string) (string, error) {
parts := strings.Split(unsetSubdomainHost, ".")
if h.isTopLevelRedirectableDomain(unsetSubdomainHost) {
//Already topped
return "", errors.New("already at top level domain")
}
for i := 0; i < len(parts); i++ {
possibleTld := parts[i:]
_, ok := h.Parent.tldMap[strings.Join(possibleTld, ".")]
if ok {
//This is tld length
tld := strings.Join(parts[i-1:], ".")
return "//" + tld, nil
}
}
return "", errors.New("unsupported top level domain given")
}

View File

@ -60,6 +60,7 @@ type ResponseRewriteRuleSet struct {
ProxyDomain string ProxyDomain string
OriginalHost string OriginalHost string
UseTLS bool UseTLS bool
NoCache bool
PathPrefix string //Vdir prefix for root, / will be rewrite to this PathPrefix string //Vdir prefix for root, / will be rewrite to this
} }
@ -243,7 +244,7 @@ func (p *ReverseProxy) logf(format string, args ...interface{}) {
} }
} }
func removeHeaders(header http.Header) { func removeHeaders(header http.Header, noCache bool) {
// Remove hop-by-hop headers listed in the "Connection" header. // Remove hop-by-hop headers listed in the "Connection" header.
if c := header.Get("Connection"); c != "" { if c := header.Get("Connection"); c != "" {
for _, f := range strings.Split(c, ",") { for _, f := range strings.Split(c, ",") {
@ -260,9 +261,16 @@ func removeHeaders(header http.Header) {
} }
} }
if header.Get("A-Upgrade") != "" { //Restore the Upgrade header if any
header.Set("Upgrade", header.Get("A-Upgrade")) if header.Get("Zr-Origin-Upgrade") != "" {
header.Del("A-Upgrade") header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
header.Del("Zr-Origin-Upgrade")
}
//Disable cache if nocache is set
if noCache {
header.Del("Cache-Control")
header.Set("Cache-Control", "no-store")
} }
} }
@ -281,6 +289,11 @@ func addXForwardedForHeader(req *http.Request) {
req.Header.Set("X-Forwarded-Proto", "http") req.Header.Set("X-Forwarded-Proto", "http")
} }
if req.Header.Get("X-Real-Ip") == "" {
//Not exists. Fill it in with client IP
req.Header.Set("X-Real-Ip", clientIP)
}
} }
} }
@ -323,7 +336,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
copyHeader(outreq.Header, req.Header) copyHeader(outreq.Header, req.Header)
// Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers. // Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers.
removeHeaders(outreq.Header) removeHeaders(outreq.Header, rrr.NoCache)
// Add X-Forwarded-For Header. // Add X-Forwarded-For Header.
addXForwardedForHeader(outreq) addXForwardedForHeader(outreq)
@ -339,7 +352,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
} }
// Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers. // Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers.
removeHeaders(res.Header) removeHeaders(res.Header, rrr.NoCache)
if p.ModifyResponse != nil { if p.ModifyResponse != nil {
if err := p.ModifyResponse(res); err != nil { if err := p.ModifyResponse(res); err != nil {

View File

@ -35,12 +35,6 @@ func NewDynamicProxy(option RouterOption) (*Router, error) {
Parent: &thisRouter, Parent: &thisRouter,
} }
//Prase the tld map for tld redirection in main router
//See Server.go declarations
if len(rawTldMap) > 0 {
json.Unmarshal(rawTldMap, &thisRouter.tldMap)
}
return &thisRouter, nil return &thisRouter, nil
} }
@ -74,12 +68,12 @@ func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
func (router *Router) StartProxyService() error { func (router *Router) StartProxyService() error {
//Create a new server object //Create a new server object
if router.server != nil { if router.server != nil {
return errors.New("Reverse proxy server already running") return errors.New("reverse proxy server already running")
} }
//Check if root route is set //Check if root route is set
if router.Root == nil { if router.Root == nil {
return errors.New("Reverse proxy router root not set") return errors.New("reverse proxy router root not set")
} }
minVersion := tls.VersionTLS10 minVersion := tls.VersionTLS10
@ -92,16 +86,6 @@ func (router *Router) StartProxyService() error {
} }
if router.Option.UseTls { if router.Option.UseTls {
/*
//Serve with TLS mode
ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config)
if err != nil {
log.Println(err)
router.Running = false
return err
}
router.tlsListener = ln
*/
router.server = &http.Server{ router.server = &http.Server{
Addr: ":" + strconv.Itoa(router.Option.Port), Addr: ":" + strconv.Itoa(router.Option.Port),
Handler: router.mux, Handler: router.mux,
@ -216,7 +200,7 @@ func (router *Router) StartProxyService() error {
func (router *Router) StopProxyService() error { func (router *Router) StopProxyService() error {
if router.server == nil { if router.server == nil {
return errors.New("Reverse proxy server already stopped") return errors.New("reverse proxy server already stopped")
} }
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
@ -251,6 +235,7 @@ func (router *Router) Restart() error {
return err return err
} }
time.Sleep(300 * time.Millisecond)
// Start the server // Start the server
err = router.StartProxyService() err = router.StartProxyService()
if err != nil { if err != nil {

View File

@ -7,7 +7,13 @@ import (
) )
/* /*
Endpoint Functions endpoint.go
author: tobychui
This script handle the proxy endpoint object actions
so proxyEndpoint can be handled like a proper oop object
Most of the functions are implemented in dynamicproxy.go
*/ */
// Get virtual directory handler from given URI // Get virtual directory handler from given URI
@ -87,3 +93,16 @@ func (ep *ProxyEndpoint) Clone() *ProxyEndpoint {
json.Unmarshal(js, &clonedProxyEndpoint) json.Unmarshal(js, &clonedProxyEndpoint)
return &clonedProxyEndpoint return &clonedProxyEndpoint
} }
// Remove this proxy endpoint from running proxy endpoint list
func (ep *ProxyEndpoint) Remove() error {
ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
return nil
}
// Write changes to runtime without respawning the proxy handler
// use prepare -> remove -> add if you change anything in the endpoint
// that effects the proxy routing src / dest
func (ep *ProxyEndpoint) UpdateToRuntime() {
ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep)
}

View File

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
@ -37,6 +38,7 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi
} }
//No hit. Try with wildcard //No hit. Try with wildcard
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)
@ -45,12 +47,24 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi
return true return true
} }
if match { if match {
targetSubdomainEndpoint = ep //targetSubdomainEndpoint = ep
return false matchProxyEndpoints = append(matchProxyEndpoints, ep)
return true
} }
return true return true
}) })
if len(matchProxyEndpoints) == 1 {
//Only 1 match
return matchProxyEndpoints[0]
} else if len(matchProxyEndpoints) > 1 {
//More than one match. Get the best match one
sort.Slice(matchProxyEndpoints, func(i, j int) bool {
return matchProxyEndpoints[i].RootOrMatchingDomain < matchProxyEndpoints[j].RootOrMatchingDomain
})
return matchProxyEndpoints[0]
}
return targetSubdomainEndpoint return targetSubdomainEndpoint
} }
@ -77,7 +91,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
requestURL := r.URL.String() requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" { if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("A-Upgrade", "websocket") r.Header.Set("Zr-Origin-Upgrade", "websocket")
wsRedirectionEndpoint := target.Domain wsRedirectionEndpoint := target.Domain
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" { if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
//Append / to the end of the redirection endpoint if not exists //Append / to the end of the redirection endpoint if not exists
@ -109,6 +123,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
ProxyDomain: target.Domain, ProxyDomain: target.Domain,
OriginalHost: originalHostHeader, OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS, UseTLS: target.RequireTLS,
NoCache: h.Parent.Option.NoCache,
PathPrefix: "", PathPrefix: "",
}) })
@ -137,7 +152,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID) r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" { if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("A-Upgrade", "websocket") r.Header.Set("Zr-Origin-Upgrade", "websocket")
wsRedirectionEndpoint := target.Domain wsRedirectionEndpoint := target.Domain
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" { if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
wsRedirectionEndpoint = wsRedirectionEndpoint + "/" wsRedirectionEndpoint = wsRedirectionEndpoint + "/"

View File

@ -97,3 +97,13 @@ func (router *Router) SetProxyRouteAsRoot(endpoint *ProxyEndpoint) error {
router.Root = endpoint router.Root = endpoint
return nil return nil
} }
// ProxyEndpoint remove provide global access by key
func (router *Router) RemoveProxyEndpointByRootname(rootnameOrMatchingDomain string) error {
targetEpt, err := router.LoadProxy(rootnameOrMatchingDomain)
if err != nil {
return err
}
return targetEpt.Remove()
}

View File

@ -25,9 +25,11 @@ type ProxyHandler struct {
type RouterOption struct { type RouterOption struct {
HostUUID string //The UUID of Zoraxy, use for heading mod HostUUID string //The UUID of Zoraxy, use for heading mod
HostVersion string //The version of Zoraxy, use for heading mod
Port int //Incoming port Port int //Incoming port
UseTls bool //Use TLS to serve incoming requsts UseTls bool //Use TLS to serve incoming requsts
ForceTLSLatest bool //Force TLS1.2 or above ForceTLSLatest bool //Force TLS1.2 or above
NoCache bool //Force set Cache-Control: no-store
ListenOnPort80 bool //Enable port 80 http listener ListenOnPort80 bool //Enable port 80 http listener
ForceHttpsRedirect bool //Force redirection of http to https endpoint ForceHttpsRedirect bool //Force redirection of http to https endpoint
TlsManager *tlscert.Manager TlsManager *tlscert.Manager

View File

@ -1,6 +1,9 @@
package geodb package geodb
import "strings" import (
"encoding/json"
"strings"
)
/* /*
Whitelist.go Whitelist.go
@ -8,11 +11,29 @@ import "strings"
This script handles whitelist related functions 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 //Geo Whitelist
func (s *Store) AddCountryCodeToWhitelist(countryCode string) { func (s *Store) AddCountryCodeToWhitelist(countryCode string, comment string) {
countryCode = strings.ToLower(countryCode) countryCode = strings.ToLower(countryCode)
s.sysdb.Write("whitelist-cn", countryCode, true) entry := WhitelistEntry{
EntryType: EntryType_CountryCode,
CC: countryCode,
Comment: comment,
}
s.sysdb.Write("whitelist-cn", countryCode, entry)
} }
func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) { func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
@ -22,20 +43,19 @@ func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool { func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode) countryCode = strings.ToLower(countryCode)
var isWhitelisted bool = false return s.sysdb.KeyExists("whitelist-cn", countryCode)
s.sysdb.Read("whitelist-cn", countryCode, &isWhitelisted)
return isWhitelisted
} }
func (s *Store) GetAllWhitelistedCountryCode() []string { func (s *Store) GetAllWhitelistedCountryCode() []*WhitelistEntry {
whitelistedCountryCode := []string{} whitelistedCountryCode := []*WhitelistEntry{}
entries, err := s.sysdb.ListTable("whitelist-cn") entries, err := s.sysdb.ListTable("whitelist-cn")
if err != nil { if err != nil {
return whitelistedCountryCode return whitelistedCountryCode
} }
for _, keypairs := range entries { for _, keypairs := range entries {
ip := string(keypairs[0]) thisWhitelistEntry := WhitelistEntry{}
whitelistedCountryCode = append(whitelistedCountryCode, ip) json.Unmarshal(keypairs[1], &thisWhitelistEntry)
whitelistedCountryCode = append(whitelistedCountryCode, &thisWhitelistEntry)
} }
return whitelistedCountryCode return whitelistedCountryCode
@ -43,8 +63,14 @@ func (s *Store) GetAllWhitelistedCountryCode() []string {
//IP Whitelist //IP Whitelist
func (s *Store) AddIPToWhiteList(ipAddr string) { func (s *Store) AddIPToWhiteList(ipAddr string, comment string) {
s.sysdb.Write("whitelist-ip", ipAddr, true) thisIpEntry := WhitelistEntry{
EntryType: EntryType_IP,
IP: ipAddr,
Comment: comment,
}
s.sysdb.Write("whitelist-ip", ipAddr, thisIpEntry)
} }
func (s *Store) RemoveIPFromWhiteList(ipAddr string) { func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
@ -52,14 +78,14 @@ func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
} }
func (s *Store) IsIPWhitelisted(ipAddr string) bool { func (s *Store) IsIPWhitelisted(ipAddr string) bool {
var isWhitelisted bool = false isWhitelisted := s.sysdb.KeyExists("whitelist-ip", ipAddr)
s.sysdb.Read("whitelist-ip", ipAddr, &isWhitelisted)
if isWhitelisted { if isWhitelisted {
//single IP whitelist entry
return true return true
} }
//Check for IP wildcard and CIRD rules //Check for IP wildcard and CIRD rules
AllWhitelistedIps := s.GetAllWhitelistedIp() AllWhitelistedIps := s.GetAllWhitelistedIpAsStringSlice()
for _, whitelistRules := range AllWhitelistedIps { for _, whitelistRules := range AllWhitelistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, whitelistRules) wildcardMatch := MatchIpWildcard(ipAddr, whitelistRules)
if wildcardMatch { if wildcardMatch {
@ -75,17 +101,29 @@ func (s *Store) IsIPWhitelisted(ipAddr string) bool {
return false return false
} }
func (s *Store) GetAllWhitelistedIp() []string { func (s *Store) GetAllWhitelistedIp() []*WhitelistEntry {
whitelistedIp := []string{} whitelistedIp := []*WhitelistEntry{}
entries, err := s.sysdb.ListTable("whitelist-ip") entries, err := s.sysdb.ListTable("whitelist-ip")
if err != nil { if err != nil {
return whitelistedIp return whitelistedIp
} }
for _, keypairs := range entries { for _, keypairs := range entries {
ip := string(keypairs[0]) //ip := string(keypairs[0])
whitelistedIp = append(whitelistedIp, ip) thisEntry := WhitelistEntry{}
json.Unmarshal(keypairs[1], &thisEntry)
whitelistedIp = append(whitelistedIp, &thisEntry)
} }
return whitelistedIp 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

@ -211,9 +211,9 @@ func removeHeaders(header http.Header) {
} }
} }
if header.Get("A-Upgrade") != "" { if header.Get("Zr-Origin-Upgrade") != "" {
header.Set("Upgrade", header.Get("A-Upgrade")) header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
header.Del("A-Upgrade") header.Del("Zr-Origin-Upgrade")
} }
} }

View File

@ -82,7 +82,7 @@ func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWrite
requestURL := r.URL.String() requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" { if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("A-Upgrade", "websocket") r.Header.Set("Zr-Origin-Upgrade", "websocket")
requestURL = strings.TrimPrefix(requestURL, "/") requestURL = strings.TrimPrefix(requestURL, "/")
u, _ := url.Parse("ws://127.0.0.1:" + strconv.Itoa(targetInstance.AssignedPort) + "/" + requestURL) u, _ := url.Parse("ws://127.0.0.1:" + strconv.Itoa(targetInstance.AssignedPort) + "/" + requestURL)
wspHandler := websocketproxy.NewProxy(u, false) wspHandler := websocketproxy.NewProxy(u, false)

View File

@ -5,22 +5,22 @@ import (
"strings" "strings"
) )
//This remove the certificates in the list where either the // This remove the certificates in the list where either the
//public key or the private key is missing // public key or the private key is missing
func getCertPairs(certFiles []string) []string { func getCertPairs(certFiles []string) []string {
crtMap := make(map[string]bool) pemMap := make(map[string]bool)
keyMap := make(map[string]bool) keyMap := make(map[string]bool)
for _, filename := range certFiles { for _, filename := range certFiles {
if filepath.Ext(filename) == ".crt" { if filepath.Ext(filename) == ".pem" {
crtMap[strings.TrimSuffix(filename, ".crt")] = true pemMap[strings.TrimSuffix(filename, ".pem")] = true
} else if filepath.Ext(filename) == ".key" { } else if filepath.Ext(filename) == ".key" {
keyMap[strings.TrimSuffix(filename, ".key")] = true keyMap[strings.TrimSuffix(filename, ".key")] = true
} }
} }
var result []string var result []string
for domain := range crtMap { for domain := range pemMap {
if keyMap[domain] { if keyMap[domain] {
result = append(result, domain) result = append(result, domain)
} }
@ -29,7 +29,7 @@ func getCertPairs(certFiles []string) []string {
return result return result
} }
//Get the cloest subdomain certificate from a list of domains // Get the cloest subdomain certificate from a list of domains
func matchClosestDomainCertificate(subdomain string, domains []string) string { func matchClosestDomainCertificate(subdomain string, domains []string) string {
var matchingDomain string = "" var matchingDomain string = ""
maxLength := 0 maxLength := 0
@ -43,18 +43,3 @@ func matchClosestDomainCertificate(subdomain string, domains []string) string {
return matchingDomain return matchingDomain
} }
//Check if a requesting domain is a subdomain of a given domain
func isSubdomain(subdomain, domain string) bool {
subdomainParts := strings.Split(subdomain, ".")
domainParts := strings.Split(domain, ".")
if len(subdomainParts) < len(domainParts) {
return false
}
for i := range domainParts {
if subdomainParts[len(subdomainParts)-1-i] != domainParts[len(domainParts)-1-i] {
return false
}
}
return true
}

View File

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDuTCCAqCgAwIBAgIBADANBgkqhkiG9w0BAQ0FADB2MQswCQYDVQQGEwJoazES
MBAGA1UECAwJSG9uZyBLb25nMRQwEgYDVQQKDAtpbXVzbGFiLmNvbTEZMBcGA1UE
AwwQWm9yYXh5IFNlbGYtaG9zdDEQMA4GA1UEBwwHSU1VU0xBQjEQMA4GA1UECwwH
SU1VU0xBQjAeFw0yMzA1MjcxMDQyNDJaFw0zODA1MjgxMDQyNDJaMHYxCzAJBgNV
BAYTAmhrMRIwEAYDVQQIDAlIb25nIEtvbmcxFDASBgNVBAoMC2ltdXNsYWIuY29t
MRkwFwYDVQQDDBBab3JheHkgU2VsZi1ob3N0MRAwDgYDVQQHDAdJTVVTTEFCMRAw
DgYDVQQLDAdJTVVTTEFCMIIBIzANBgkqhkiG9w0BAQEFAAOCARAAMIIBCwKCAQIA
xav3Qq4DBooHsGW9m+r0dgjI832grX2c0Z6MJQQoE7B6wfpUI0OyfRugTXyXoiRZ
gLxuROgiCUmp8FaLbl7RsvbImMbCPo3D/RbCT1aJCNXLZ0a7yvcDYc6woQW4nUyk
ohHfT2otcu+OYS6aYRZuXGsKTAqPSwEXRMtr89wkPgZPsrCD27LFHBOmIcVABDvF
KRuiwHWSHhFfU5n1AZLyYeYoLNQ9fZPvzPpkMD+HMKi4MMwr/vLE0DwU5jSfVFq+
cd68zVihp9N/T77yah5EIH9CYm4m8Acs4bfL8DALxnaSN3KmGw6J35rOXrJvJLdh
t42PDROmQrXN8uG8wGkBiBkCAwEAAaNQME4wHQYDVR0OBBYEFLhXihE+1K6MoL0P
Nx5htfuSatpiMB8GA1UdIwQYMBaAFLhXihE+1K6MoL0PNx5htfuSatpiMAwGA1Ud
EwQFMAMBAf8wDQYJKoZIhvcNAQENBQADggECAMCn0ed1bfLefGvoQJV/q+X9p61U
HunSFJAAhp0N2Q3tq/zjIu0kJX7N0JBciEw2c0ZmqJIqR8V8Im/h/4XuuOR+53hg
opOSPo39ww7mpxyBlQm63v1nXcNQcvw4U0JqXQ4Kyv8cgX7DIuyjRWHQpc5+6joy
L5Nz5hzQbgpnPdHQEMorfnm8q6bWg/291IAV3ZA9Z6T5gn4YuyjeUdDczQtpT6nu
1iTNPqtO6R3aeTVT+OSJT9sH2MHfDAsf371HBM6MzM/5QBc/62Bgau7NUjNKeSEA
EtUBil8wBHwT7vOtqbyNk5FHEfoCpYsQtP7AtEo10izKCQpDXPftfiJefkOY
-----END CERTIFICATE-----

View File

@ -6,7 +6,6 @@ import (
"embed" "embed"
"encoding/pem" "encoding/pem"
"io" "io"
"io/ioutil"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@ -15,12 +14,19 @@ import (
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
type Manager struct { type CertCache struct {
CertStore string Cert *x509.Certificate
verbal bool PubKey string
PriKey string
} }
//go:embed localhost.crt localhost.key type Manager struct {
CertStore string //Path where all the certs are stored
LoadedCerts []*CertCache //A list of loaded certs
verbal bool
}
//go:embed localhost.pem localhost.key
var buildinCertStore embed.FS var buildinCertStore embed.FS
func NewManager(certStore string, verbal bool) (*Manager, error) { func NewManager(certStore string, verbal bool) (*Manager, error) {
@ -28,14 +34,99 @@ func NewManager(certStore string, verbal bool) (*Manager, error) {
os.MkdirAll(certStore, 0775) os.MkdirAll(certStore, 0775)
} }
pubKey := "./tmp/localhost.pem"
priKey := "./tmp/localhost.key"
//Check if this is initial setup
if !utils.FileExists(pubKey) {
buildInPubKey, _ := buildinCertStore.ReadFile(filepath.Base(pubKey))
os.WriteFile(pubKey, buildInPubKey, 0775)
}
if !utils.FileExists(priKey) {
buildInPriKey, _ := buildinCertStore.ReadFile(filepath.Base(priKey))
os.WriteFile(priKey, buildInPriKey, 0775)
}
thisManager := Manager{ thisManager := Manager{
CertStore: certStore, CertStore: certStore,
verbal: verbal, LoadedCerts: []*CertCache{},
verbal: verbal,
}
err := thisManager.UpdateLoadedCertList()
if err != nil {
return nil, err
} }
return &thisManager, nil return &thisManager, nil
} }
// Update domain mapping from file
func (m *Manager) UpdateLoadedCertList() error {
//Get a list of certificates from file
domainList, err := m.ListCertDomains()
if err != nil {
return err
}
//Load each of the certificates into memory
certList := []*CertCache{}
for _, certname := range domainList {
//Read their certificate into memory
pubKey := filepath.Join(m.CertStore, certname+".pem")
priKey := filepath.Join(m.CertStore, certname+".key")
certificate, err := tls.LoadX509KeyPair(pubKey, priKey)
if err != nil {
log.Println("Certificate loaded failed: " + certname)
continue
}
for _, thisCert := range certificate.Certificate {
loadedCert, err := x509.ParseCertificate(thisCert)
if err != nil {
//Error pasring cert, skip this byte segment
continue
}
thisCacheEntry := CertCache{
Cert: loadedCert,
PubKey: pubKey,
PriKey: priKey,
}
certList = append(certList, &thisCacheEntry)
}
}
//Replace runtime cert array
m.LoadedCerts = certList
return nil
}
// Match cert by CN
func (m *Manager) CertMatchExists(serverName string) bool {
for _, certCacheEntry := range m.LoadedCerts {
if certCacheEntry.Cert.VerifyHostname(serverName) == nil || certCacheEntry.Cert.Issuer.CommonName == serverName {
return true
}
}
return false
}
// Get cert entry by matching server name, return pubKey and priKey if found
// check with CertMatchExists before calling to the load function
func (m *Manager) GetCertByX509CNHostname(serverName string) (string, string) {
for _, certCacheEntry := range m.LoadedCerts {
if certCacheEntry.Cert.VerifyHostname(serverName) == nil || certCacheEntry.Cert.Issuer.CommonName == serverName {
return certCacheEntry.PubKey, certCacheEntry.PriKey
}
}
return "", ""
}
// Return a list of domains by filename
func (m *Manager) ListCertDomains() ([]string, error) { func (m *Manager) ListCertDomains() ([]string, error) {
filenames, err := m.ListCerts() filenames, err := m.ListCerts()
if err != nil { if err != nil {
@ -48,8 +139,9 @@ func (m *Manager) ListCertDomains() ([]string, error) {
return filenames, nil return filenames, nil
} }
// Return a list of cert files (public and private keys)
func (m *Manager) ListCerts() ([]string, error) { func (m *Manager) ListCerts() ([]string, error) {
certs, err := ioutil.ReadDir(m.CertStore) certs, err := os.ReadDir(m.CertStore)
if err != nil { if err != nil {
return []string{}, err return []string{}, err
} }
@ -64,44 +156,52 @@ func (m *Manager) ListCerts() ([]string, error) {
return filenames, nil return filenames, nil
} }
// Get a certificate from disk where its certificate matches with the helloinfo
func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) { func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) {
//Check if the domain corrisponding cert exists //Check if the domain corrisponding cert exists
pubKey := "./tmp/localhost.crt" pubKey := "./tmp/localhost.pem"
priKey := "./tmp/localhost.key" priKey := "./tmp/localhost.key"
//Check if this is initial setup if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".pem")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
if !utils.FileExists(pubKey) { //Direct hit
buildInPubKey, _ := buildinCertStore.ReadFile(filepath.Base(pubKey)) pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".pem")
os.WriteFile(pubKey, buildInPubKey, 0775)
}
if !utils.FileExists(priKey) {
buildInPriKey, _ := buildinCertStore.ReadFile(filepath.Base(priKey))
os.WriteFile(priKey, buildInPriKey, 0775)
}
if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".crt")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".crt")
priKey = filepath.Join(m.CertStore, helloInfo.ServerName+".key") priKey = filepath.Join(m.CertStore, helloInfo.ServerName+".key")
} else if m.CertMatchExists(helloInfo.ServerName) {
//Use x509
pubKey, priKey = m.GetCertByX509CNHostname(helloInfo.ServerName)
} else { } else {
domainCerts, _ := m.ListCertDomains() //Fallback to legacy method of matching certificates
cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts) /*
if cloestDomainCert != "" { domainCerts, _ := m.ListCertDomains()
//There is a matching parent domain for this subdomain. Use this instead. cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts)
pubKey = filepath.Join(m.CertStore, cloestDomainCert+".crt") if cloestDomainCert != "" {
priKey = filepath.Join(m.CertStore, cloestDomainCert+".key") //There is a matching parent domain for this subdomain. Use this instead.
} else if m.DefaultCertExists() { pubKey = filepath.Join(m.CertStore, cloestDomainCert+".pem")
//Use default.crt and default.key priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
pubKey = filepath.Join(m.CertStore, "default.crt") } else if m.DefaultCertExists() {
//Use default.pem and default.key
pubKey = filepath.Join(m.CertStore, "default.pem")
priKey = filepath.Join(m.CertStore, "default.key")
if m.verbal {
log.Println("No matching certificate found. Serving with default")
}
} else {
if m.verbal {
log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName)
}
}*/
if m.DefaultCertExists() {
//Use default.pem and default.key
pubKey = filepath.Join(m.CertStore, "default.pem")
priKey = filepath.Join(m.CertStore, "default.key") priKey = filepath.Join(m.CertStore, "default.key")
if m.verbal { //if m.verbal {
log.Println("No matching certificate found. Serving with default") // log.Println("No matching certificate found. Serving with default")
} //}
} else { } else {
if m.verbal { //if m.verbal {
log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName) // log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName)
} //}
} }
} }
@ -117,17 +217,17 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err
// Check if both the default cert public key and private key exists // Check if both the default cert public key and private key exists
func (m *Manager) DefaultCertExists() bool { func (m *Manager) DefaultCertExists() bool {
return utils.FileExists(filepath.Join(m.CertStore, "default.crt")) && utils.FileExists(filepath.Join(m.CertStore, "default.key")) return utils.FileExists(filepath.Join(m.CertStore, "default.pem")) && utils.FileExists(filepath.Join(m.CertStore, "default.key"))
} }
// Check if the default cert exists returning seperate results for pubkey and prikey // Check if the default cert exists returning seperate results for pubkey and prikey
func (m *Manager) DefaultCertExistsSep() (bool, bool) { func (m *Manager) DefaultCertExistsSep() (bool, bool) {
return utils.FileExists(filepath.Join(m.CertStore, "default.crt")), utils.FileExists(filepath.Join(m.CertStore, "default.key")) return utils.FileExists(filepath.Join(m.CertStore, "default.pem")), utils.FileExists(filepath.Join(m.CertStore, "default.key"))
} }
// Delete the cert if exists // Delete the cert if exists
func (m *Manager) RemoveCert(domain string) error { func (m *Manager) RemoveCert(domain string) error {
pubKey := filepath.Join(m.CertStore, domain+".crt") pubKey := filepath.Join(m.CertStore, domain+".pem")
priKey := filepath.Join(m.CertStore, domain+".key") priKey := filepath.Join(m.CertStore, domain+".key")
if utils.FileExists(pubKey) { if utils.FileExists(pubKey) {
err := os.Remove(pubKey) err := os.Remove(pubKey)
@ -143,6 +243,9 @@ func (m *Manager) RemoveCert(domain string) error {
} }
} }
//Update the cert list
m.UpdateLoadedCertList()
return nil return nil
} }
@ -171,15 +274,11 @@ func IsValidTLSFile(file io.Reader) bool {
return false return false
} }
// Check if the certificate is a valid TLS/SSL certificate // Check if the certificate is a valid TLS/SSL certificate
return cert.IsCA == false && cert.KeyUsage&x509.KeyUsageDigitalSignature != 0 && cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0 return !cert.IsCA && cert.KeyUsage&x509.KeyUsageDigitalSignature != 0 && cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0
} else if strings.Contains(block.Type, "PRIVATE KEY") { } else if strings.Contains(block.Type, "PRIVATE KEY") {
// The file contains a private key // The file contains a private key
_, err := x509.ParsePKCS1PrivateKey(block.Bytes) _, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil { return err == nil
// Handle the error
return false
}
return true
} else { } else {
return false return false
} }

View File

@ -217,7 +217,11 @@ func getWebsiteStatusWithLatency(url string) (bool, int64, int) {
} }
func getWebsiteStatus(url string) (int, error) { func getWebsiteStatus(url string) (int, error) {
resp, err := http.Get(url) client := http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Get(url)
if err != nil { if err != nil {
//Try replace the http with https and vise versa //Try replace the http with https and vise versa
rewriteURL := "" rewriteURL := ""
@ -227,7 +231,7 @@ func getWebsiteStatus(url string) (int, error) {
rewriteURL = strings.ReplaceAll(url, "http://", "https://") rewriteURL = strings.ReplaceAll(url, "http://", "https://")
} }
resp, err = http.Get(rewriteURL) resp, err = client.Get(rewriteURL)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") { if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") {
//Invalid downstream reverse proxy settings, but it is online //Invalid downstream reverse proxy settings, but it is online

View File

@ -72,6 +72,7 @@ func (ws *WebServer) HandlePortChange(w http.ResponseWriter, r *http.Request) {
utils.SendErrorResponse(w, err.Error()) utils.SendErrorResponse(w, err.Error())
return return
} }
utils.SendOK(w) utils.SendOK(w)
} }

View File

@ -48,6 +48,14 @@ func ReverseProxtInit() {
SystemWideLogger.Println("Force latest TLS mode disabled. Minimum TLS version is set to v1.0") SystemWideLogger.Println("Force latest TLS mode disabled. Minimum TLS version is set to v1.0")
} }
developmentMode := false
sysdb.Read("settings", "devMode", &developmentMode)
if useTls {
SystemWideLogger.Println("Development mode enabled. Using no-store Cache Control policy")
} else {
SystemWideLogger.Println("Development mode disabled. Proxying with default Cache Control policy")
}
listenOnPort80 := false listenOnPort80 := false
sysdb.Read("settings", "listenP80", &listenOnPort80) sysdb.Read("settings", "listenP80", &listenOnPort80)
if listenOnPort80 { if listenOnPort80 {
@ -74,9 +82,11 @@ func ReverseProxtInit() {
dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{ dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
HostUUID: nodeUUID, HostUUID: nodeUUID,
HostVersion: version,
Port: inboundPort, Port: inboundPort,
UseTls: useTls, UseTls: useTls,
ForceTLSLatest: forceLatestTLSVersion, ForceTLSLatest: forceLatestTLSVersion,
NoCache: developmentMode,
ListenOnPort80: listenOnPort80, ListenOnPort80: listenOnPort80,
ForceHttpsRedirect: forceHttpsRedirect, ForceHttpsRedirect: forceHttpsRedirect,
TlsManager: tlsCertManager, TlsManager: tlsCertManager,
@ -325,10 +335,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
} }
//Update utm if exists //Update utm if exists
if uptimeMonitor != nil { UpdateUptimeMonitorTargets()
uptimeMonitor.Config.Targets = GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter)
uptimeMonitor.CleanRecords()
}
utils.SendOK(w) utils.SendOK(w)
} }
@ -741,7 +748,7 @@ func HandleUpdatePort80Listener(w http.ResponseWriter, r *http.Request) {
} else if enabled == "false" { } else if enabled == "false" {
sysdb.Write("settings", "listenP80", false) sysdb.Write("settings", "listenP80", false)
SystemWideLogger.Println("Disabling port 80 listener") SystemWideLogger.Println("Disabling port 80 listener")
dynamicProxyRouter.UpdatePort80ListenerState(true) dynamicProxyRouter.UpdatePort80ListenerState(false)
} else { } else {
utils.SendErrorResponse(w, "invalid mode given: "+enabled) utils.SendErrorResponse(w, "invalid mode given: "+enabled)
} }
@ -789,6 +796,30 @@ func HandleManagementProxyCheck(w http.ResponseWriter, r *http.Request) {
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
} }
func HandleDevelopmentModeChange(w http.ResponseWriter, r *http.Request) {
enableDevelopmentModeStr, err := utils.GetPara(r, "enable")
if err != nil {
//Load the current development mode toggle state
js, _ := json.Marshal(dynamicProxyRouter.Option.NoCache)
utils.SendJSONResponse(w, string(js))
} else {
//Write changes to runtime
enableDevelopmentMode := false
if enableDevelopmentModeStr == "true" {
enableDevelopmentMode = true
}
//Write changes to runtime
dynamicProxyRouter.Option.NoCache = enableDevelopmentMode
//Write changes to database
sysdb.Write("settings", "devMode", enableDevelopmentMode)
utils.SendOK(w)
}
}
// Handle incoming port set. Change the current proxy incoming port // Handle incoming port set. Change the current proxy incoming port
func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) { func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
newIncomingPort, err := utils.PostPara(r, "incoming") newIncomingPort, err := utils.PostPara(r, "incoming")

View File

@ -15,6 +15,8 @@ import (
This script holds the static resources router This script holds the static resources router
for the reverse proxy service for the reverse proxy service
If you are looking for reverse proxy handler, see Server.go in mod/dynamicproxy/
*/ */
func FSHandler(handler http.Handler) http.Handler { func FSHandler(handler http.Handler) http.Handler {

35
src/routingrule.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"net/http"
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy"
)
/*
Routing Rule
This script handle special routing rules for some utilities functions
*/
// Register the system build-in routing rules into the core
func registerBuildInRoutingRules() {
//Cloudflare email decoder
//It decode the email address if you are proxying a cloudflare protected site
//[email-protected] -> real@email.com
dynamicProxyRouter.AddRoutingRules(&dynamicproxy.RoutingRule{
ID: "cloudflare-decoder",
MatchRule: func(r *http.Request) bool {
return strings.HasSuffix(r.RequestURI, "cloudflare-static/email-decode.min.js")
},
RoutingHandler: func(w http.ResponseWriter, r *http.Request) {
decoder := "function fixObfuscatedEmails(){let t=document.getElementsByClassName(\"__cf_email__\");for(let e=0;e<t.length;e++){let r=t[e],l=r.getAttribute(\"data-cfemail\");if(l){let a=decrypt(l);r.setAttribute(\"href\",\"mailto:\"+a),r.innerHTML=a}}}function decrypt(t){let e=\"\",r=parseInt(t.substr(0,2),16);for(let l=2;l<t.length;l+=2){let a=parseInt(t.substr(l,2),16)^r;e+=String.fromCharCode(a)}try{e=decodeURIComponent(escape(e))}catch(f){console.error(f)}return e}fixObfuscatedEmails();"
w.Header().Set("Content-type", "text/javascript")
w.Write([]byte(decoder))
},
Enabled: false,
UseSystemAccessControl: false,
})
}

View File

@ -238,4 +238,7 @@ func startupSequence() {
func finalSequence() { func finalSequence() {
//Start ACME renew agent //Start ACME renew agent
acmeRegisterSpecialRoutingRule() acmeRegisterSpecialRoutingRule()
//Inject routing rules
registerBuildInRoutingRules()
} }

View File

@ -615,8 +615,12 @@
<p>Whitelist a certain IP or IP range</p> <p>Whitelist a certain IP or IP range</p>
<div class="ui form"> <div class="ui form">
<div class="field"> <div class="field">
<label>IP Address</label> <label>IP Address</label>
<input id="ipAddressInputWhitelist" type="text" placeholder="IP Address"> <input id="ipAddressInputWhitelist" type="text" placeholder="IP Address">
</div>
<div class="field">
<label>Remarks (Optional)</label>
<input id="ipAddressCommentsWhitelist" type="text" placeholder="Comments or remarks for this IP range">
</div> </div>
<button id="addIpButton" onclick="addIpWhitelist();" class="ui basic green button"> <button id="addIpButton" onclick="addIpWhitelist();" class="ui basic green button">
<i class="green add icon"></i> Whitelist IP <i class="green add icon"></i> Whitelist IP
@ -634,6 +638,7 @@
<thead> <thead>
<tr> <tr>
<th>IP Address</th> <th>IP Address</th>
<th>Remarks</th>
<th>Remove</th> <th>Remove</th>
</tr> </tr>
</thead> </thead>
@ -793,11 +798,12 @@
if (data.length === 0) { if (data.length === 0) {
$('#whitelistIpTable').append(` $('#whitelistIpTable').append(`
<tr> <tr>
<td colspan="2"><i class="green check circle icon"></i>There are no whitelisted IP addresses</td> <td colspan="3"><i class="green check circle icon"></i>There are no whitelisted IP addresses</td>
</tr> </tr>
`); `);
} else { } else {
$.each(data, function(index, ip) { $.each(data, function(index, ipEntry) {
let ip = ipEntry.IP;
let icon = "globe icon"; let icon = "globe icon";
if (isLAN(ip)){ if (isLAN(ip)){
icon = "desktop icon"; icon = "desktop icon";
@ -807,6 +813,7 @@
$('#whitelistIpTable').append(` $('#whitelistIpTable').append(`
<tr class="whitelistItem" ip="${encodeURIComponent(ip)}"> <tr class="whitelistItem" ip="${encodeURIComponent(ip)}">
<td><i class="${icon}"></i> ${ip}</td> <td><i class="${icon}"></i> ${ip}</td>
<td>${ipEntry.Comment}</td>
<td><button class="ui icon basic mini red button" onclick="removeIpWhitelist('${ip}');"><i class="trash alternate icon"></i></button></td> <td><button class="ui icon basic mini red button" onclick="removeIpWhitelist('${ip}');"><i class="trash alternate icon"></i></button></td>
</tr> </tr>
`); `);
@ -1003,6 +1010,7 @@
function addIpWhitelist(){ function addIpWhitelist(){
let targetIp = $("#ipAddressInputWhitelist").val().trim(); let targetIp = $("#ipAddressInputWhitelist").val().trim();
let remarks = $("#ipAddressCommentsWhitelist").val().trim();
if (targetIp == ""){ if (targetIp == ""){
alert("IP address is empty") alert("IP address is empty")
return return
@ -1016,7 +1024,7 @@
$.ajax({ $.ajax({
url: "/api/whitelist/ip/add", url: "/api/whitelist/ip/add",
type: "POST", type: "POST",
data: {ip: targetIp.toLowerCase()}, data: {ip: targetIp.toLowerCase(), "comment": remarks},
success: function(response) { success: function(response) {
if (response.error !== undefined) { if (response.error !== undefined) {
msgbox(response.error, false, 6000); msgbox(response.error, false, 6000);
@ -1025,6 +1033,7 @@
} }
$("#ipAddressInputWhitelist").val(""); $("#ipAddressInputWhitelist").val("");
$("#ipAddressCommentsWhitelist").val("");
$("#ipAddressInputWhitelist").parent().remvoeClass("error"); $("#ipAddressInputWhitelist").parent().remvoeClass("error");
}, },
error: function() { error: function() {

View File

@ -15,42 +15,19 @@
</div> </div>
<div class="ui divider"></div> <div class="ui divider"></div>
<h4>Default Certificates</h4> <h3>Hosts Certificates</h3>
<p>When there are no matching certificate for the requested server name, reverse proxy router will always fallback to this one.<br>Note that you need both of them uploaded for it to fallback properly</p>
<table class="ui very basic unstackable celled table">
<thead>
<tr><th class="no-sort">Key Type</th>
<th class="no-sort">Exists</th>
</tr></thead>
<tbody>
<tr>
<td><i class="globe icon"></i> Default Public Key</td>
<td id="pubkeyExists"></td>
</tr>
<tr>
<td><i class="lock icon"></i> Default Private Key</td>
<td id="prikeyExists"></td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
<div class="ui buttons">
<button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
<button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
</div>
<div class="ui divider"></div>
<h4>Sub-domain Certificates</h4>
<p>Provide certificates for multiple domains reverse proxy</p> <p>Provide certificates for multiple domains reverse proxy</p>
<div class="ui fluid form"> <div class="ui fluid form">
<div class="three fields"> <div class="three fields">
<div class="field"> <div class="field">
<label>Server Name (Domain)</label> <label>Server Name (Domain)</label>
<input type="text" id="certdomain" placeholder="example.com / blog.example.com"> <input type="text" id="certdomain" placeholder="example.com / blog.example.com">
<small><i class="exclamation circle yellow icon"></i> Match the server name with your CN/DNS entry in certificate for faster resolve time</small>
</div> </div>
<div class="field"> <div class="field">
<label>Public Key (.pem)</label> <label>Public Key (.pem)</label>
<input type="file" id="pubkeySelector" onchange="handleFileSelect(event, 'pub')"> <input type="file" id="pubkeySelector" onchange="handleFileSelect(event, 'pub')">
<small>or .crt files in order systems</small>
</div> </div>
<div class="field"> <div class="field">
<label>Private Key (.key)</label> <label>Private Key (.key)</label>
@ -63,14 +40,33 @@
<div id="certUploadSuccMsg" class="ui green message" style="display:none;"> <div id="certUploadSuccMsg" class="ui green message" style="display:none;">
<i class="ui checkmark icon"></i> Certificate for domain <span id="certUploadingDomain"></span> uploaded. <i class="ui checkmark icon"></i> Certificate for domain <span id="certUploadingDomain"></span> uploaded.
</div> </div>
<br> <div class="ui message">
<h4>Tips about Server Names & SNI</h4>
<div class="ui bulleted list">
<div class="item">
If you have two subdomains like <code>a.example.com</code> and <code>b.example.com</code> ,
for faster response speed, you might want to setup them one by one (i.e. having two seperate certificate for
<code>a.example.com</code> and <code>b.example.com</code>).
</div>
<div class="item">
If you have a wildcard certificate that covers <code>*.example.com</code>,
you can just enter <code>example.com</code> as server name to add a certificate.
</div>
<div class="item">
If you have a certificate contain multiple host, you can enter the first domain in your certificate
and Zoraxy will try to match the remaining CN/DNS for you.
</div>
</div>
</div>
<p>Current list of loaded certificates</p>
<div> <div>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;"> <div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table class="ui sortable unstackable celled table"> <table class="ui sortable unstackable basic celled table">
<thead> <thead>
<tr><th>Domain</th> <tr><th>Domain</th>
<th>Last Update</th> <th>Last Update</th>
<th>Expire At</th> <th>Expire At</th>
<th class="no-sort">Renew</th>
<th class="no-sort">Remove</th> <th class="no-sort">Remove</th>
</tr></thead> </tr></thead>
<tbody id="certifiedDomainList"> <tbody id="certifiedDomainList">
@ -81,15 +77,34 @@
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button> <button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
</div> </div>
<div class="ui message">
<h4><i class="info circle icon"></i> Sub-domain Certificates</h4>
If you have 3rd or even 4th level subdomains like <code>blog.example.com</code> or <code>en.blog.example.com</code> ,
depending on your certificates coverage, you might need to setup them one by one (i.e. having two seperate certificate for <code>a.example.com</code> and <code>b.example.com</code>).<br>
If you have a wildcard certificate that covers <code>*.example.com</code>, you can just enter <code>example.com</code> as server name in the form below to add a certificate.
</div>
<div class="ui divider"></div> <div class="ui divider"></div>
<h4>Certificate Authority (CA) and Auto Renew (ACME)</h4> <h3>Fallback Certificate</h3>
<p>When there are no matching certificate for the requested server name, reverse proxy router will always fallback to this one.<br>Note that you need both of them uploaded for it to fallback properly</p>
<table class="ui very basic unstackable celled table">
<thead>
<tr><th class="no-sort">Key Type</th>
<th class="no-sort">Found</th>
</tr></thead>
<tbody>
<tr>
<td><i class="globe icon"></i> Fallback Public Key</td>
<td id="pubkeyExists"></td>
</tr>
<tr>
<td><i class="lock icon"></i> Fallback Private Key</td>
<td id="prikeyExists"></td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
<div class="ui buttons">
<button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
<button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
</div>
<div class="ui divider"></div>
<h3>Certificate Authority (CA) and Auto Renew (ACME)</h3>
<p>Management features regarding CA and ACME</p> <p>Management features regarding CA and ACME</p>
<h4>Prefered Certificate Authority</h4>
<p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p> <p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p>
<div class="ui fluid form"> <div class="ui fluid form">
<div class="field"> <div class="field">
@ -112,12 +127,12 @@
<button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button> <button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
</div><br> </div><br>
<h5>Certificate Renew / Generation (ACME) Settings</h5> <h5>Certificate Renew / Generation (ACME) Settings</h5>
<div class="ui basic segment"> <div class="ui basic segment acmeRenewStateWrapper">
<h4 class="ui header" id="acmeAutoRenewer"> <h4 class="ui header" id="acmeAutoRenewer">
<i class="red circle icon"></i> <i class="white remove icon"></i>
<div class="content"> <div class="content">
<span id="acmeAutoRenewerStatus">Disabled</span> <span id="acmeAutoRenewerStatus">Disabled</span>
<div class="sub header">Auto-Renewer Status</div> <div class="sub header">ACME Auto-Renewer</div>
</div> </div>
</h4> </h4>
</div> </div>
@ -130,6 +145,110 @@
$("#defaultCA").dropdown(); $("#defaultCA").dropdown();
//Renew certificate by button press
function renewCertificate(domain, btn=undefined){
let defaultCA = $("#defaultCA").dropdown("get value");
if (defaultCA.trim() == ""){
defaultCA = "Let's Encrypt";
}
//Get a new cert using ACME
msgbox("Requesting certificate via " + defaultCA +"...");
//Request ACME for certificate
if (btn != undefined){
$(btn).addClass('disabled');
$(btn).html(`<i class="ui loading spinner icon"></i>`);
}
obtainCertificate(domain, defaultCA.trim(), function(succ){
if (btn != undefined){
$(btn).removeClass('disabled');
if (succ){
$(btn).html(`<i class="ui green check icon"></i>`);
}else{
$(btn).html(`<i class="ui red times icon"></i>`);
}
setTimeout(function(){
initManagedDomainCertificateList();
}, 3000);
}
});
}
/*
Obtain Certificate via ACME
*/
// Obtain certificate from API, only support one domain
function obtainCertificate(domains, usingCa = "Let's Encrypt", callback=undefined) {
//Load the ACME email from server side
let acmeEmail = "";
$.get("/api/acme/autoRenew/email", function(data){
if (data != "" && data != undefined && data != null){
acmeEmail = data;
}
let filename = "";
let email = acmeEmail;
if (acmeEmail == ""){
msgbox("Unable to obtain certificate: ACME email not set", false, 8000);
if (callback != undefined){
callback(false);
}
return;
}
if (filename.trim() == "" && !domains.includes(",")){
//Zoraxy filename are the matching name for domains.
//Use the same as domains
filename = domains;
}else if (filename != "" && !domains.includes(",")){
//Invalid settings. Force the filename to be same as domain
//if there are only 1 domain
filename = domains;
}else{
msgbox("Filename cannot be empty for certs containing multiple domains.")
if (callback != undefined){
callback(false);
}
return;
}
$.ajax({
url: "/api/acme/obtainCert",
method: "GET",
data: {
domains: domains,
filename: filename,
email: email,
ca: usingCa,
},
success: function(response) {
if (response.error) {
console.log("Error:", response.error);
// Show error message
msgbox(response.error, false, 12000);
if (callback != undefined){
callback(false);
}
} else {
console.log("Certificate installed successfully");
// Show success message
msgbox("Certificate installed successfully");
if (callback != undefined){
callback(false);
}
}
},
error: function(error) {
console.log("Failed to install certificate:", error);
}
});
});
}
//Delete the certificate by its domain //Delete the certificate by its domain
function deleteCertificate(domain){ function deleteCertificate(domain){
if (confirm("Confirm delete certificate for " + domain + " ?")){ if (confirm("Confirm delete certificate for " + domain + " ?")){
@ -154,6 +273,12 @@
//Initialize the current default CA options //Initialize the current default CA options
$.get("/api/acme/autoRenew/email", function(data){ $.get("/api/acme/autoRenew/email", function(data){
$("#prefACMEEmail").val(data); $("#prefACMEEmail").val(data);
if (data.trim() == ""){
//acme email is not yet set
$(".renewButton").addClass('disabled');
}else{
$(".renewButton").removeClass('disabled');
}
}); });
$.get("/api/acme/autoRenew/ca", function(data){ $.get("/api/acme/autoRenew/ca", function(data){
@ -167,7 +292,13 @@
//Set the status of the acme enable icon //Set the status of the acme enable icon
function setACMEEnableStates(enabled){ function setACMEEnableStates(enabled){
$("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled"); $("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled");
$("#acmeAutoRenewer").find("i").attr("class", enabled?"green circle icon":"red circle icon"); if (enabled){
$(".acmeRenewStateWrapper").addClass("enabled");
}else{
$(".acmeRenewStateWrapper").removeClass("enabled");
}
$("#acmeAutoRenewer").find("i").attr("class", enabled?"white circle check icon":"white circle times icon");
} }
initAcmeStatus(); initAcmeStatus();
@ -187,6 +318,9 @@
success: function(data){ success: function(data){
if (data.error != undefined){ if (data.error != undefined){
msgbox(data.error, false); msgbox(data.error, false);
}else{
//Update the renew button states
$(".renewButton").removeClass('disabled');
} }
} }
}); });
@ -223,13 +357,14 @@
<td>${entry.Domain}</td> <td>${entry.Domain}</td>
<td>${entry.LastModifiedDate}</td> <td>${entry.LastModifiedDate}</td>
<td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td> <td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td>
<td><button title="Renew Certificate" class="ui mini basic icon button renewButton" onclick="renewCertificate('${entry.Domain}', this);"><i class="ui green refresh icon"></i></button></td>
<td><button title="Delete key-pair" class="ui mini basic red icon button" onclick="deleteCertificate('${entry.Domain}');"><i class="ui red trash icon"></i></button></td> <td><button title="Delete key-pair" class="ui mini basic red icon button" onclick="deleteCertificate('${entry.Domain}');"><i class="ui red trash icon"></i></button></td>
</tr>`); </tr>`);
}); });
if (data.length == 0){ if (data.length == 0){
$("#certifiedDomainList").append(`<tr> $("#certifiedDomainList").append(`<tr>
<td colspan="4"><i class="ui times circle icon"></i> No valid keypairs found</td> <td colspan="4"><i class="ui times red circle icon"></i> No valid keypairs found</td>
</tr>`); </tr>`);
} }
} }

View File

@ -3,15 +3,15 @@
<h2>HTTP Proxy</h2> <h2>HTTP Proxy</h2>
<p>Proxy HTTP server with HTTP or HTTPS for multiple hosts. If you are only proxying for one host / domain, use Default Site instead.</p> <p>Proxy HTTP server with HTTP or HTTPS for multiple hosts. If you are only proxying for one host / domain, use Default Site instead.</p>
</div> </div>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em; min-width: 400px;"> <div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table class="ui celled sortable unstackable basic compact table"> <table class="ui celled sortable unstackable compact table">
<thead> <thead>
<tr> <tr>
<th>Host</th> <th>Host</th>
<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: 7.2em;">Actions</th> <th class="no-sort" style="min-width:100px;">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody id="httpProxyList"> <tbody id="httpProxyList">
@ -19,6 +19,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<button class="ui icon right floated basic button" onclick="listProxyEndpoints();"><i class="green refresh icon"></i> Refresh</button> <button class="ui icon right floated basic button" onclick="listProxyEndpoints();"><i class="green refresh icon"></i> Refresh</button>
<br><br> <br><br>
</div> </div>

View File

@ -110,6 +110,7 @@
$("#ruleAddSucc").stop().finish().slideDown("fast").delay(3000).slideUp("fast"); $("#ruleAddSucc").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
} }
initRedirectionRuleList(); initRedirectionRuleList();
resetForm();
} }
}); });
} }
@ -141,7 +142,7 @@
$.get("/api/redirect/list", function(data){ $.get("/api/redirect/list", function(data){
data.forEach(function(entry){ data.forEach(function(entry){
$("#redirectionRuleList").append(`<tr> $("#redirectionRuleList").append(`<tr>
<td>${entry.RedirectURL} </td> <td><a href="${entry.RedirectURL}" target="_blank">${entry.RedirectURL}</a></td>
<td>${entry.TargetURL}</td> <td>${entry.TargetURL}</td>
<td>${entry.ForwardChildpath?"<i class='ui green checkmark icon'></i>":"<i class='ui red remove icon'></i>"}</td> <td>${entry.ForwardChildpath?"<i class='ui green checkmark icon'></i>":"<i class='ui red remove icon'></i>"}</td>
<td>${entry.StatusCode==307?"Temporary Redirect (307)":"Moved Permanently (301)"}</td> <td>${entry.StatusCode==307?"Temporary Redirect (307)":"Moved Permanently (301)"}</td>

View File

@ -198,19 +198,20 @@
//Set the new proxy root option //Set the new proxy root option
function setProxyRoot(btn=undefined){ function setProxyRoot(btn=undefined){
if (btn != undefined){
$(btn).addClass("disabled");
}
var newpr = $("#proxyRoot").val(); var newpr = $("#proxyRoot").val();
if (newpr.trim() == ""){ if (newpr.trim() == "" && currentDefaultSiteOption == 0){
$("#proxyRoot").parent().addClass('error'); //Fill in the web server info
return newpr = "127.0.0.1:" + $("#webserv_listenPort").val();
}else{ $("#proxyRoot").val(newpr);
$("#proxyRoot").parent().removeClass('error');
} }
var rootReqTls = $("#rootReqTLS")[0].checked; var rootReqTls = $("#rootReqTLS")[0].checked;
if (btn != undefined){
$(btn).addClass("disabled");
}
//proxy mode or redirect mode, check for input values //proxy mode or redirect mode, check for input values
var defaultSiteValue = ""; var defaultSiteValue = "";
if (currentDefaultSiteOption == 1){ if (currentDefaultSiteOption == 1){

View File

@ -118,7 +118,6 @@
$("#advanceProxyRules").accordion(); $("#advanceProxyRules").accordion();
//New Proxy Endpoint //New Proxy Endpoint
function newProxyEndpoint(){ function newProxyEndpoint(){
var rootname = $("#rootname").val(); var rootname = $("#rootname").val();
@ -164,7 +163,7 @@
$("#proxyDomain").val(""); $("#proxyDomain").val("");
credentials = []; credentials = [];
updateTable(); updateTable();
reloadUptimeList();
//Check if it is a new subdomain and TLS enabled //Check if it is a new subdomain and TLS enabled
if ($("#tls").checkbox("is checked")){ if ($("#tls").checkbox("is checked")){
confirmBox("Request new SSL Cert for this subdomain?", function(choice){ confirmBox("Request new SSL Cert for this subdomain?", function(choice){
@ -177,7 +176,12 @@
//Get a new cert using ACME //Get a new cert using ACME
msgbox("Requesting certificate via " + defaultCA +"..."); msgbox("Requesting certificate via " + defaultCA +"...");
console.log("Trying to get a new certificate via ACME"); console.log("Trying to get a new certificate via ACME");
obtainCertificate(rootname, defaultCA.trim());
//Request ACME for certificate, see cert.html component
obtainCertificate(rootname, defaultCA.trim(), function(){
// Renew the parent certificate list
initManagedDomainCertificateList();
});
}else{ }else{
msgbox("Proxy Endpoint Added"); msgbox("Proxy Endpoint Added");
} }
@ -193,7 +197,7 @@
//Generic functions for delete rp endpoints //Generic functions for delete rp endpoints
function deleteEndpoint(epoint){ function deleteEndpoint(epoint){
epoint = decodeURIComponent(epoint); epoint = decodeURIComponent(epoint).hexDecode();
if (confirm("Confirm remove proxy for :" + epoint + "?")){ if (confirm("Confirm remove proxy for :" + epoint + "?")){
$.ajax({ $.ajax({
url: "/api/proxy/del", url: "/api/proxy/del",
@ -201,6 +205,7 @@
success: function(){ success: function(){
listProxyEndpoints(); listProxyEndpoints();
msgbox("Proxy Rule Deleted", true); msgbox("Proxy Rule Deleted", true);
reloadUptimeList();
} }
}) })
} }
@ -300,67 +305,7 @@
updateTable(); updateTable();
} }
/*
Obtain Certificate via ACME
*/
//Load the ACME email from server side
let acmeEmail = "";
$.get("/api/acme/autoRenew/email", function(data){
if (data != "" && data != undefined && data != null){
acmeEmail = data;
}
});
// Obtain certificate from API, only support one domain
function obtainCertificate(domains, usingCa = "Let's Encrypt") {
let filename = "";
let email = acmeEmail;
if (acmeEmail == ""){
let rootDomain = domains.split(".").pop();
email = "admin@" + rootDomain;
}
if (filename.trim() == "" && !domains.includes(",")){
//Zoraxy filename are the matching name for domains.
//Use the same as domains
filename = domains;
}else if (filename != "" && !domains.includes(",")){
//Invalid settings. Force the filename to be same as domain
//if there are only 1 domain
filename = domains;
}else{
parent.msgbox("Filename cannot be empty for certs containing multiple domains.")
return;
}
$.ajax({
url: "/api/acme/obtainCert",
method: "GET",
data: {
domains: domains,
filename: filename,
email: email,
ca: usingCa,
},
success: function(response) {
if (response.error) {
console.log("Error:", response.error);
// Show error message
msgbox(response.error, false, 12000);
} else {
console.log("Certificate installed successfully");
// Show success message
msgbox("Certificate installed successfully");
// Renew the parent certificate list
initManagedDomainCertificateList();
}
},
error: function(error) {
console.log("Failed to install certificate:", error);
}
});
}
//Update v3.0.0 //Update v3.0.0
//Since some proxy rules now contains wildcard characters //Since some proxy rules now contains wildcard characters

View File

@ -44,7 +44,7 @@
<div class="content"> <div class="content">
<span id="country"></span> <span id="country"></span>
<div class="sub header" id="countryList"> <div class="sub header" id="countryList">
<i class="ui loading circle notch icon"></i> Resolving GeoIP
</div> </div>
</div> </div>
</h5> </h5>
@ -96,13 +96,18 @@
Advance Settings Advance Settings
</div> </div>
<div class="content"> <div class="content">
<p>If you have no idea what are these, you can leave them as default :)</p>
<div id="tlsMinVer" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;"> <div id="tlsMinVer" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
<input type="checkbox"> <input type="checkbox">
<label>Force TLS v1.2 or above<br> <label>Force TLS v1.2 or above<br>
<small>(Enhance security, but not compatible with legacy browsers)</small></label> <small>(Enhance security, but not compatible with legacy browsers)</small></label>
</div> </div>
<br> <br>
<div id="developmentMode" class="ui toggle checkbox" style="margin-top: 0.6em;">
<input type="checkbox">
<label>Development Mode<br>
<small>(Set Cache-Control to no-store so browser will always fetch new contents from your sites)</small></label>
</div>
<br>
</div> </div>
</div> </div>
</div> </div>
@ -436,6 +441,30 @@
} }
initTlsVersionSetting(); initTlsVersionSetting();
function initDevelopmentMode(){
$.get("/api/proxy/developmentMode", function(data){
if (data === true){
$("#developmentMode").checkbox("set checked")
}else{
$("#developmentMode").checkbox("set unchecked")
}
//Bind change events
$("#developmentMode").off("change").on("change", function(data){
let enableDevMode = ($(this).find("input[type='checkbox']")[0].checked);
$.get("/api/proxy/developmentMode?enable=" + enableDevMode, function(data){
if (enableDevMode){
msgbox("Development mode enabled");
}else{
msgbox("Development mode disabled");
}
});
});
});
}
initDevelopmentMode();
function initTlsSetting(){ function initTlsSetting(){
$.get("/api/cert/tls", function(data){ $.get("/api/cert/tls", function(data){
if (data == true){ if (data == true){

View File

@ -3,11 +3,33 @@
<h2>TCP Proxy</h2> <h2>TCP Proxy</h2>
<p>Proxy traffic flow on layer 3 via TCP/IP</p> <p>Proxy traffic flow on layer 3 via TCP/IP</p>
</div> </div>
<button class="ui basic orange button" id="addProxyConfigButton"><i class="ui add icon"></i> Add Proxy Config</button>
<button class="ui basic circular right floated icon button" onclick="initProxyConfigList();" title="Refresh List"><i class="ui green refresh icon"></i></button>
<div class="ui divider"></div> <div class="ui divider"></div>
<div class="ui basic segment" id="addproxyConfig" style="display:none;"> <div class="ui basic segment" style="margin-top: 0;">
<h3>TCP Proxy Config</h3> <h4>TCP Proxy Rules</h4>
<p>A list of TCP proxy rules created on this host. To enable them, use the toggle button on the right.</p>
<div style="overflow-x: auto; min-height: 400px;">
<table id="proxyTable" class="ui celled unstackable table">
<thead>
<tr>
<th>Name</th>
<th>Port/Addr A</th>
<th>Port/Addr B</th>
<th>Mode</th>
<th>Timeout (s)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<button class="ui basic right floated button" onclick="initProxyConfigList();" title="Refresh List"><i class="ui green refresh icon"></i>Refresh</button>
<br><br>
</div>
<div class="ui divider"></div>
<div class="ui basic segment" id="addproxyConfig">
<h4>Add or Edit TCP Proxy</h4>
<p>Create or edit a new proxy instance</p> <p>Create or edit a new proxy instance</p>
<form id="tcpProxyForm" class="ui form"> <form id="tcpProxyForm" class="ui form">
<div class="field" style="display:none;"> <div class="field" style="display:none;">
@ -39,11 +61,10 @@
<option value="starter">Starter</option> <option value="starter">Starter</option>
</select> </select>
</div> </div>
<button id="addTcpProxyButton" class="ui basic button" type="submit"><i class="ui blue add icon"></i> Create</button> <button id="addTcpProxyButton" class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
<button id="editTcpProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);"><i class="ui blue save icon"></i> Update</button> <button id="editTcpProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);" style="display:none;"><i class="ui green check icon"></i> Update</button>
<button class="ui basic red button" onclick="event.preventDefault(); cancelTCPProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button> <button class="ui basic red button" onclick="event.preventDefault(); cancelTCPProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
<div class="ui basic inverted segment" style="background-color: #414141; border-radius: 0.6em;"> <div class="ui basic inverted segment" style="background: var(--theme_background_inverted); border-radius: 0.6em;">
<h3>Proxy Mode Instructions</h3>
<p>TCP Proxy support the following TCP sockets proxy modes</p> <p>TCP Proxy support the following TCP sockets proxy modes</p>
<table class="ui celled padded inverted basic table"> <table class="ui celled padded inverted basic table">
<thead> <thead>
@ -108,28 +129,6 @@
</table> </table>
</div> </div>
</form> </form>
<div class="ui divider"></div>
</div>
<div class="ui basic segment" style="margin-top: 0;">
<h3>TCP Proxy Configs</h3>
<p>A list of TCP proxy configs created on this host. To enable them, use the toggle button on the right.</p>
<div style="overflow-x: auto; min-height: 400px;">
<table id="proxyTable" class="ui celled unstackable table">
<thead>
<tr>
<th>Name</th>
<th>Port/Addr A</th>
<th>Port/Addr B</th>
<th>Mode</th>
<th>Timeout (s)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div> </div>
</div> </div>
<script> <script>
@ -138,6 +137,13 @@
$("#tcpProxyForm .dropdown").dropdown(); $("#tcpProxyForm .dropdown").dropdown();
$('#tcpProxyForm').on('submit', function(event) { $('#tcpProxyForm').on('submit', function(event) {
event.preventDefault(); event.preventDefault();
//Check if update mode
if ($("#editTcpProxyButton").is(":visible")){
confirmEditTCPProxyConfig(event);
return;
}
var form = $(this); var form = $(this);
var formValid = validateTCPProxyConfig(form); var formValid = validateTCPProxyConfig(form);
@ -166,22 +172,15 @@
}); });
}); });
//Add proxy button pressed. Show add TCP proxy menu
$("#addProxyConfigButton").on("click", function(){
$('#addproxyConfig').slideToggle('fast');
$("#addTcpProxyButton").show();
$("#editTcpProxyButton").hide();
});
function clearTCPProxyAddEditForm(){ function clearTCPProxyAddEditForm(){
$('#tcpProxyForm input, #tcpProxyForm select').val(''); $('#tcpProxyForm input, #tcpProxyForm select').val('');
$('#tcpProxyForm select').dropdown('clear'); $('#tcpProxyForm select').dropdown('clear');
} }
function cancelTCPProxyEdit(event) { function cancelTCPProxyEdit(event=undefined) {
clearTCPProxyAddEditForm(); clearTCPProxyAddEditForm();
$('#addproxyConfig').slideUp('fast'); $("#addTcpProxyButton").show();
$("#editTcpProxyButton").hide();
} }
function validateTCPProxyConfig(form){ function validateTCPProxyConfig(form){
@ -231,7 +230,7 @@
proxyConfigs.forEach(function(config) { proxyConfigs.forEach(function(config) {
var runningLogo = 'Stopped'; var runningLogo = 'Stopped';
var runningClass = "stopped"; var runningClass = "stopped";
var startButton = `<button onclick="startTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="play icon"></i> Start Proxy</button>`; var startButton = `<button onclick="startTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="green play icon"></i> Start Proxy</button>`;
if (config.Running){ if (config.Running){
runningLogo = 'Running'; runningLogo = 'Running';
startButton = `<button onclick="stopTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="red stop icon"></i> Stop Proxy</button>`; startButton = `<button onclick="stopTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="red stop icon"></i> Stop Proxy</button>`;
@ -354,8 +353,8 @@
msgbox("Config Updated"); msgbox("Config Updated");
} }
initProxyConfigList(); initProxyConfigList();
clearTCPProxyAddEditForm(); cancelTCPProxyEdit();
$("#addproxyConfig").slideUp("fast");
}, },
error: function() { error: function() {
msgbox('An error occurred while processing the request', false); msgbox('An error occurred while processing the request', false);

View File

@ -3,166 +3,179 @@
<h2>Utilities</h2> <h2>Utilities</h2>
<p>You might find these tools or information helpful when setting up your gateway server</p> <p>You might find these tools or information helpful when setting up your gateway server</p>
</div> </div>
<div class="ui divider"></div> <div class="ui top attached tabular menu">
<a class="nettools item active" data-tab="tab1"><i class="ui user circle blue icon"></i> Accounts</a>
<a class="nettools item" data-tab="tab2">Toolbox</a>
<a class="nettools item" data-tab="tab3">System</a>
</div>
<div class="ui bottom attached tab segment nettoolstab active" data-tab="tab1">
<div class="selfauthOnly"> <div class="extAuthOnly" style="display:none;">
<h3>Account Management</h3> <div class="ui basic segment">
<p>Functions to help management the current account</p> <i class="ui green circle check icon"></i> Account options are not available due to -noauth flag is set to true.
<div class="ui basic segment">
<h5><i class="chevron down icon"></i> Change Password</h5>
<div class="ui form">
<div class="field">
<label>Current Password</label>
<input type="password" name="oldPassword" placeholder="Current Password">
</div>
<div class="field">
<label>New Password</label>
<input type="password" name="newPassword" placeholder="New Password">
</div>
<div class="field">
<label>Confirm New Password</label>
<input type="password" name="confirmNewPassword" placeholder="Confirm New Password">
</div>
<button class="ui basic button" onclick="changePassword()"><i class="ui teal key icon"></i> Change Password</button>
</div>
<div id="passwordChangeSuccMsg" class="ui green message" style="display:none;">
<i class="ui circle checkmark green icon "></i> Password Updated
</div> </div>
</div> </div>
<div class="ui divider"></div> <div class="selfauthOnly">
<h3>Forget Password Email</h3> <h3>Change Password</h3>
<p>The following SMTP settings help you to reset your password in case you have lost your management account.</p> <p>Update the current account credentials</p>
<form id="email-form" class="ui form"> <div class="ui basic segment">
<div class="field"> <h5><i class="chevron down icon"></i> Change Password</h5>
<label>Sender Address</label> <div class="ui form">
<input type="text" name="senderAddr" placeholder="E.g. noreply@zoraxy.arozos.com"> <div class="field">
</div> <label>Current Password</label>
<div class="field"> <input type="password" name="oldPassword" placeholder="Current Password">
<p><i class="caret down icon"></i> Connection setup for email service provider</p>
<div class="fields">
<div class="twelve wide field">
<label>SMTP Provider Hostname</label>
<input type="text" name="hostname" placeholder="E.g. mail.gandi.net">
</div> </div>
<div class="field">
<label>New Password</label>
<input type="password" name="newPassword" placeholder="New Password">
</div>
<div class="field">
<label>Confirm New Password</label>
<input type="password" name="confirmNewPassword" placeholder="Confirm New Password">
</div>
<button class="ui basic button" onclick="changePassword()"><i class="ui teal key icon"></i> Change Password</button>
</div>
<div class="four wide field"> <div id="passwordChangeSuccMsg" class="ui green message" style="display:none;">
<label>Port</label> <i class="ui circle checkmark green icon "></i> Password Updated
<input type="number" name="port" placeholder="E.g. 587" value="587">
</div>
</div> </div>
</div> </div>
<div class="ui divider"></div>
<h3>Forget Password Email</h3>
<p>The following SMTP settings help you to reset your password in case you have lost your management account.</p>
<form id="email-form" class="ui form">
<div class="field">
<label>Sender Address</label>
<input type="text" name="senderAddr" placeholder="E.g. noreply@zoraxy.arozos.com">
</div>
<div class="field">
<p><i class="caret down icon"></i> Connection setup for email service provider</p>
<div class="fields">
<div class="twelve wide field">
<label>SMTP Provider Hostname</label>
<input type="text" name="hostname" placeholder="E.g. mail.gandi.net">
</div>
<div class="field"> <div class="four wide field">
<p><i class="caret down icon"></i> Credentials for SMTP server authentications</p> <label>Port</label>
<div class="two fields"> <input type="number" name="port" placeholder="E.g. 587" value="587">
<div class="field">
<label>Sender Username</label>
<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>
<div class="field">
<label>Sender Password</label>
<input type="password" name="password" placeholder="Password of the email account">
<small>Leave empty to use the old password</small>
</div>
<p> <i class="caret down icon"></i> Email for sending account reset link</p> <div class="field">
<div class="field"> <p><i class="caret down icon"></i> Credentials for SMTP server authentications</p>
<label>Admin / Receiver Address</label> <div class="two fields">
<input type="text" name="recvAddr" placeholder="E.g. personalEmail@gmail.com"> <div class="field">
</div> <label>Sender Username</label>
<input type="text" name="username" placeholder="E.g. admin">
</div>
<button class="ui basic button" type="submit"><i class="blue save icon"></i> Set SMTP Configs</button> <div class="field">
<button class="ui basic button" onclick="event.preventDefault(); sendTestEmail(this);"><i class="teal mail icon"></i> Send Test Email</button> <label>Sender Domain</label>
</form> <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 class="field">
<label>Sender Password</label>
<input type="password" name="password" placeholder="Password of the email account">
<small>Leave empty to use the old password</small>
</div>
<p> <i class="caret down icon"></i> Email for sending account reset link</p>
<div class="field">
<label>Admin / Receiver Address</label>
<input type="text" name="recvAddr" placeholder="E.g. personalEmail@gmail.com">
</div>
<button class="ui basic button" type="submit"><i class="blue save icon"></i> Set SMTP Configs</button>
<button class="ui basic button" onclick="event.preventDefault(); sendTestEmail(this);"><i class="teal mail icon"></i> Send Test Email</button>
</form>
</div>
</div> </div>
<div class="ui divider"></div> <div class="ui bottom attached tab segment nettoolstab" data-tab="tab2">
<h3> IP Address to CIDR</h3> <h3> IP Address to CIDR</h3>
<p>No experience with CIDR notations? Here are some tools you can use to make setting up easier.</p> <p>No experience with CIDR notations? Here are some tools you can use to make setting up easier.</p>
<div class="ui basic segment"> <div class="ui basic segment">
<h5><i class="chevron down icon"></i> IP Range to CIDR Conversion</h5> <h5><i class="chevron down icon"></i> IP Range to CIDR Conversion</h5>
<div class="ui message"> <div class="ui message">
<i class="info circle icon"></i> Note that the CIDR generated here covers additional IP address before or after the given range. If you need more details settings, please use CIDR with a smaller range and add additional IPs for detail range adjustment. <i class="info circle icon"></i> Note that the CIDR generated here covers additional IP address before or after the given range. If you need more details settings, please use CIDR with a smaller range and add additional IPs for detail range adjustment.
</div>
<div class="ui input">
<input type="text" placeholder="Start IP" id="startIpInput">
</div>
<div class="ui input">
<input type="text" placeholder="End IP" id="endIpInput">
</div>
<br>
<button style="margin-top: 0.6em;" class="ui basic button" onclick="convertToCIDR()">Convert</button>
<p>Results: <div id="cidrOutput">N/A</div></p>
</div> </div>
<div class="ui input">
<input type="text" placeholder="Start IP" id="startIpInput">
</div>
<div class="ui input">
<input type="text" placeholder="End IP" id="endIpInput">
</div>
<br>
<button style="margin-top: 0.6em;" class="ui basic button" onclick="convertToCIDR()">Convert</button>
<p>Results: <div id="cidrOutput">N/A</div></p>
</div>
<div class="ui basic segment"> <div class="ui basic segment">
<h5><i class="chevron down icon"></i> CIDR to IP Range Conversion</h5> <h5><i class="chevron down icon"></i> CIDR to IP Range Conversion</h5>
<div class="ui action input"> <div class="ui action input">
<input type="text" placeholder="CIDR" id="cidrInput"> <input type="text" placeholder="CIDR" id="cidrInput">
<button class="ui basic button" onclick="convertToIPRange()">Convert</button> <button class="ui basic button" onclick="convertToIPRange()">Convert</button>
</div>
<p>Results: <div id="ipRangeOutput">N/A</div></p>
</div> </div>
<p>Results: <div id="ipRangeOutput">N/A</div></p> <div class="ui divider"></div>
</div> </div>
<!-- Config Tools --> <div class="ui bottom attached tab segment nettoolstab" data-tab="tab3">
<div class="ui divider"></div> <!-- Config Tools -->
<h3>System Backup & Restore</h3> <h3>System Backup & Restore</h3>
<p>Options related to system backup, migrate and restore.</p> <p>Options related to system backup, migrate and restore.</p>
<button class="ui basic button" onclick="showSideWrapper('snippet/configTools.html');">Open Config Tools</button> <button class="ui basic button" onclick="showSideWrapper('snippet/configTools.html');">Open Config Tools</button>
<!-- System Information --> <div class="ui divider"></div>
<div class="ui divider"></div> <!-- System Information -->
<div id="zoraxyinfo"> <div id="zoraxyinfo">
<h3 class="ui header"> <h3 class="ui header">
System Information System Information
</h3> </h3>
<p>Basic information about this zoraxy host</p> <p>Basic information about this zoraxy host</p>
<table class="ui very basic collapsing celled table"> <table class="ui very basic collapsing celled table">
<tbody> <tbody>
<tr> <tr>
<td>Host UUID</td> <td>Host UUID</td>
<td class="uuid"></td> <td class="uuid"></td>
</tr> </tr>
<tr> <tr>
<td>Version</td> <td>Version</td>
<td class="version"></td> <td class="version"></td>
</tr> </tr>
<tr> <tr>
<td>Build</td> <td>Build</td>
<td class="development"></td> <td class="development"></td>
</tr> </tr>
<tr> <tr>
<td>Running Since</td> <td>Running Since</td>
<td class="boottime"></td> <td class="boottime"></td>
</tr> </tr>
<tr> <tr>
<td>ZeroTier Linked</td> <td>ZeroTier Linked</td>
<td class="zt"></td> <td class="zt"></td>
</tr> </tr>
<tr> <tr>
<td>Enable SSH Loopback</td> <td>Enable SSH Loopback</td>
<td class="sshlb"></td> <td class="sshlb"></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p>Zoraxy is developed by tobychui for <a href="//imuslab.com" target="_blank">imuslab</a> and open source under <a href="https://www.gnu.org/licenses/agpl-3.0.txt">AGPL</a></p> <p>Zoraxy is developed by tobychui for <a href="//imuslab.com" target="_blank">imuslab</a> and open source under <a href="https://www.gnu.org/licenses/agpl-3.0.txt">AGPL</a></p>
</div>
</div> </div>
<br> <br>
</div> </div>
<script> <script>
$('.menu .nettools.item').tab();
/* /*
Account Password utilities Account Password utilities
*/ */
@ -171,6 +184,7 @@
if (data == 0){ if (data == 0){
//Using external auth manager. Hide options //Using external auth manager. Hide options
$(".selfauthOnly").hide(); $(".selfauthOnly").hide();
$(".extAuthOnly").show();
} }
}); });

View File

@ -30,7 +30,7 @@
<h4>Edit Virtual Directory Routing Rules</h4> <h4>Edit Virtual Directory Routing Rules</h4>
<p>The following are the list of Virtual Directories currently handled by the host router above</p> <p>The following are the list of Virtual Directories currently handled by the host router above</p>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;"> <div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table class="ui celled sortable basic unstackable compact table"> <table class="ui celled sortable unstackable compact table">
<thead> <thead>
<tr> <tr>
<th>Virtual Directory</th> <th>Virtual Directory</th>
@ -162,6 +162,7 @@
//List the vdirs //List the vdirs
console.log(data); console.log(data);
data.forEach(vdir => { data.forEach(vdir => {
var tlsIcon = "";
if (vdir.RequireTLS){ if (vdir.RequireTLS){
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`; tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
if (vdir.SkipCertValidations){ if (vdir.SkipCertValidations){

View File

@ -4,7 +4,7 @@
<p>A simple static web server that serve html css and js files</p> <p>A simple static web server that serve html css and js files</p>
</div> </div>
<div class="ui divider"></div> <div class="ui divider"></div>
<div class="ui basic segment"> <div class="ui basic segment webservRunningStateWrapper">
<h4 class="ui header" id="webservRunningState"> <h4 class="ui header" id="webservRunningState">
<i class="green circle icon"></i> <i class="green circle icon"></i>
<div class="content"> <div class="content">
@ -102,12 +102,14 @@
function setWebServerRunningState(running){ function setWebServerRunningState(running){
if (running){ if (running){
$("#webserv_enable").parent().checkbox("set checked"); $("#webserv_enable").parent().checkbox("set checked");
$("#webservRunningState").find("i").attr("class", "green circle icon"); $("#webservRunningState").find("i").attr("class", "white circle check icon");
$("#webservRunningState").find(".webserv_status").text("Running"); $("#webservRunningState").find(".webserv_status").text("Running");
$(".webservRunningStateWrapper").addClass("enabled")
}else{ }else{
$("#webserv_enable").parent().checkbox("set unchecked"); $("#webserv_enable").parent().checkbox("set unchecked");
$("#webservRunningState").find("i").attr("class", "red circle icon"); $("#webservRunningState").find("i").attr("class", "white circle times icon");
$("#webservRunningState").find(".webserv_status").text("Stopped"); $("#webservRunningState").find(".webserv_status").text("Stopped");
$(".webservRunningStateWrapper").removeClass("enabled")
} }
} }

View File

@ -26,9 +26,12 @@
<div class="ui right floated buttons menutoggle" style="padding-top: 2px;"> <div class="ui right floated buttons menutoggle" style="padding-top: 2px;">
<button class="ui basic icon button" onclick="$('.toolbar').fadeToggle('fast');"><i class="content icon"></i></button> <button class="ui basic icon button" onclick="$('.toolbar').fadeToggle('fast');"><i class="content icon"></i></button>
</div> </div>
<div class="ui right floated buttons" style="padding-top: 2px;"> <div class="ui right floated buttons" style="padding-top: 2px; padding-right: 0.4em;">
<button class="ui basic icon button" onclick="logout();"><i class="sign-out icon"></i></button> <button class="ui basic white icon button" onclick="logout();"><i class="sign-out icon"></i></button>
</div> </div>
<!-- <div class="ui right floated buttons" style="padding-top: 2px;">
<button id="themeColorButton" class="ui icon button" onclick="toggleTheme();"><i class="sun icon"></i></button>
</div> -->
</div> </div>
<div class="wrapper"> <div class="wrapper">
<div class="toolbar"> <div class="toolbar">
@ -52,8 +55,6 @@
<a class="item" tag="tcpprox"> <a class="item" tag="tcpprox">
<i class="simplistic exchange icon"></i> TCP Proxy <i class="simplistic exchange icon"></i> TCP Proxy
</a> </a>
<div class="ui divider menudivider">Access & Connections</div> <div class="ui divider menudivider">Access & Connections</div>
<a class="item" tag="cert"> <a class="item" tag="cert">
<i class="simplistic lock icon"></i> TLS / SSL certificates <i class="simplistic lock icon"></i> TLS / SSL certificates
@ -71,14 +72,13 @@
<a class="item" tag="zgrok"> <a class="item" tag="zgrok">
<i class="simplistic podcast icon"></i> Service Expose Proxy <i class="simplistic podcast icon"></i> Service Expose Proxy
</a> </a>
<div class="ui divider menudivider">Others</div>
<div class="ui divider menudivider">Hosting</div>
<a class="item" tag="webserv"> <a class="item" tag="webserv">
<i class="simplistic globe icon"></i> Static Web Server <i class="simplistic globe icon"></i> Static Web Server
</a> </a>
<a class="item" tag="utm">
<div class="ui divider menudivider">Others</div> <i class="simplistic time icon"></i> Uptime Monitor
</a>
<a class="item" tag="networktool"> <a class="item" tag="networktool">
<i class="simplistic terminal icon"></i> Network Tools <i class="simplistic terminal icon"></i> Network Tools
</a> </a>
@ -245,6 +245,16 @@
}); });
} }
function toggleTheme(){
if ($("body").hasClass("darkTheme")){
$("body").removeClass("darkTheme")
$("#themeColorButton").html(`<i class="ui moon icon"></i>`);
}else{
$("body").addClass("darkTheme");
$("#themeColorButton").html(`<i class="ui sun icon"></i>`);
}
}
function getTabButtonById(targetTabId){ function getTabButtonById(targetTabId){
let targetTabBtn = undefined; let targetTabBtn = undefined;
$("#mainmenu").find(".item").each(function(){ $("#mainmenu").find(".item").each(function(){

View File

@ -2,16 +2,66 @@
index.html style overwrite index.html style overwrite
*/ */
:root{ :root{
--theme_grey: #414141;
--theme_lgrey: #f6f6f6;
--theme_green: #3c9c63;
--theme_fcolor: #979797;
--theme_advance: #f8f8f9;
--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_green: linear-gradient(270deg, #27e7ff, #00ca52);
--theme_red: linear-gradient(203deg, rgba(250,172,38,1) 17%, rgba(202,0,37,1) 78%);
} }
/* Theme Color Definations */
body:not(.darkTheme){
--theme_bg: #f6f6f6;
--theme_bg_primary: #ffffff;
--theme_bg_secondary: #ffffff;
--theme_bg_active: #ececec;
--theme_highlight: #a9d1f3;
--theme_bg_inverted: #27292d;
--theme_advance: #f8f8f9;
--item_color: #5e5d5d;
--item_color_select: rgba(0,0,0,.87);
--text_color: #414141;
--input_color: white;
--divider_color: #cacaca;
--text_color_inverted: #fcfcfc;
--button_text_color: #878787;
--button_border_color: #dedede;
}
body.darkTheme{
--theme_bg: #27292d;
--theme_bg_primary: #3d3f47;
--theme_bg_secondary: #373a42;
--theme_highlight: #6682c4;
--theme_bg_active: #292929;
--theme_bg_inverted: #f8f8f9;
--theme_advance: #333333;
--item_color: #cacaca;
--text_color: #fcfcfc;
--text_color_secondary: #dfdfdf;
--input_color: black;
--divider_color: #3b3b3b;
--item_color_select: rgba(255, 255, 255, 0.87);
--text_color_inverted: #414141;
--button_text_color: #e9e9e9;
--button_border_color: #646464;
}
/* Theme Toggle Css */
#themeColorButton{
background-color: black;
color: var(--text_color_inverted);
}
body.darkTheme #themeColorButton{
background-color: white;
}
body{ body{
background-color:#f6f6f6; background-color:var(--theme_bg);
color: #414141; color: var(--text_color);
} }
.functiontab{ .functiontab{
@ -32,9 +82,9 @@ body{
padding: 0.4em; padding: 0.4em;
padding-left: 1.2em; padding-left: 1.2em;
padding-right: 1.2em; padding-right: 1.2em;
background-color: #ffffff; background-color: var(--theme_bg_secondary);
margin-bottom: 1em; margin-bottom: 1em;
border-bottom: 1px solid rgb(226, 226, 226); border-bottom: 1px solid var(--theme_highlight);
position: fixed; position: fixed;
top: 0; top: 0;
width: 100%; width: 100%;
@ -68,7 +118,7 @@ body{
display: inline-block; display: inline-block;
width: calc(100% - 240px); width: calc(100% - 240px);
vertical-align: top; vertical-align: top;
background-color: white; background-color: var(--theme_bg_primary);
border-radius: 1em; border-radius: 1em;
margin-right: 2em; margin-right: 2em;
} }
@ -301,13 +351,18 @@ body{
} }
.ui.menu .item{ .ui.menu .item{
color: #5e5d5d; color: var(--item_color);
}
.ui.menu .item:hover{
color: var(--item_color_select) !important;
} }
.ui.segment{ .ui.segment{
box-shadow: none !important; box-shadow: none !important;
} }
.ui.secondary.vertical.menu .active.item{ .ui.secondary.vertical.menu .active.item{
background: var(--theme_background); background: var(--theme_background);
font-weight: 600; font-weight: 600;
@ -318,11 +373,6 @@ body{
animation: blinker 3s ease-in-out infinite; animation: blinker 3s ease-in-out infinite;
} }
.ui.important.basic.segment{
background: linear-gradient(217deg, rgba(234,238,175,1) 16%, rgba(254,255,242,1) 78%);
border-radius: 1em;
}
.basic.segment.advanceoptions{ .basic.segment.advanceoptions{
background-color: #f7f7f7; background-color: #f7f7f7;
border-radius: 1em; border-radius: 1em;
@ -522,13 +572,59 @@ body{
position: absolute; position: absolute;
bottom: 0.3em; bottom: 0.3em;
left: 0.2em; left: 0.2em;
font-size: 2em; font-size: 1.4em;
color:rgb(224, 224, 224); color:rgb(224, 224, 224);
opacity: 0.7; opacity: 0.7;
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
} }
/*
ACME Renewer Status Panel
*/
.acmeRenewStateWrapper{
padding: 1em;
border-radius: 1em !important;
}
.acmeRenewStateWrapper .ui.header, .acmeRenewStateWrapper .sub.header{
color: white !important;
}
.acmeRenewStateWrapper:not(.enabled){
background: var(--theme_red) !important;
}
.acmeRenewStateWrapper.enabled{
background: var(--theme_green) !important;
}
/*
Static Web Server
*/
.webservRunningStateWrapper{
padding: 1em;
border-radius: 1em !important;
}
.webservRunningStateWrapper .ui.header, .webservRunningStateWrapper .sub.header{
color: white !important;
}
.webservRunningStateWrapper:not(.enabled){
background: var(--theme_red) !important;
}
.webservRunningStateWrapper.enabled{
background: var(--theme_green) !important;
}
/* /*
Uptime Monitor Uptime Monitor
*/ */

View File

@ -191,6 +191,7 @@
success: function(data){ success: function(data){
if (data.error != undefined){ if (data.error != undefined){
parent.msgbox(data.error, false, 5000); parent.msgbox(data.error, false, 5000);
}else{ }else{
parent.msgbox("Email updated"); parent.msgbox("Email updated");
$(btn).html(`<i class="green check icon"></i>`); $(btn).html(`<i class="green check icon"></i>`);
@ -214,14 +215,18 @@
$("#enableCertAutoRenew").parent().checkbox("set unchecked"); $("#enableCertAutoRenew").parent().checkbox("set unchecked");
enableTrigerOnChangeEvent = true; enableTrigerOnChangeEvent = true;
} }
if (parent && parent.setACMEEnableStates){
parent.setACMEEnableStates(!enabled);
}
}else{ }else{
$("#enableToggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast"); $("#enableToggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
if (parent && parent.setACMEEnableStates){
parent.setACMEEnableStates(enabled);
}
} }
}); });
if (parent && parent.setACMEEnableStates){
parent.setACMEEnableStates(enabled);
}
} }

View File

@ -45,7 +45,7 @@
</b></p> </b></p>
<form class="ui form" id="uploadForm" action="/api/conf/import" method="POST" enctype="multipart/form-data"> <form class="ui form" id="uploadForm" action="/api/conf/import" method="POST" enctype="multipart/form-data">
<input type="file" name="file" id="fileInput" accept=".zip"> <input type="file" name="file" id="fileInput" accept=".zip">
<button style="margin-top: 0.6em;" class="ui basic button" type="submit"><i class="ui green upload icon"></i> Upload</button> <button style="margin-top: 0.6em;" class="ui basic button" type="submit"><i class="ui green upload icon"></i> Restore & Exit</button>
</form> </form>
<small>The current config will be backup to ./conf_old{timestamp}. If you screw something up, you can always restore it by ssh to your host and restore the configs from the old folder.</small> <small>The current config will be backup to ./conf_old{timestamp}. If you screw something up, you can always restore it by ssh to your host and restore the configs from the old folder.</small>
<br><br> <br><br>

View File

@ -20,6 +20,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strconv"
"strings" "strings"
"time" "time"
@ -151,6 +152,48 @@ func HandleUptimeMonitorListing(w http.ResponseWriter, r *http.Request) {
} }
} }
/*
Static Web Server
*/
// Handle port change, if root router is using internal static web server
// update the root router as well
func HandleStaticWebServerPortChange(w http.ResponseWriter, r *http.Request) {
newPort, err := utils.PostInt(r, "port")
if err != nil {
utils.SendErrorResponse(w, "invalid port number given")
return
}
if dynamicProxyRouter.Root.DefaultSiteOption == dynamicproxy.DefaultSite_InternalStaticWebServer {
//Update the root site as well
newDraftingRoot := dynamicProxyRouter.Root.Clone()
newDraftingRoot.Domain = "127.0.0.1:" + strconv.Itoa(newPort)
activatedNewRoot, err := dynamicProxyRouter.PrepareProxyRoute(newDraftingRoot)
if err != nil {
utils.SendErrorResponse(w, "unable to update root routing rule")
return
}
//Replace the root
dynamicProxyRouter.Root = activatedNewRoot
SaveReverseProxyConfig(newDraftingRoot)
}
err = staticWebServer.ChangePort(strconv.Itoa(newPort))
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
/*
mDNS Scanning
*/
// Handle listing current registered mdns nodes // Handle listing current registered mdns nodes
func HandleMdnsListing(w http.ResponseWriter, r *http.Request) { func HandleMdnsListing(w http.ResponseWriter, r *http.Request) {
if mdnsScanner == nil { if mdnsScanner == nil {