Merge pull request #289 from tobychui/v3.1.1

v3.1.1 update
This commit is contained in:
Toby Chui 2024-09-04 21:35:21 +08:00 committed by GitHub
commit d5315e5b8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 7809 additions and 5890 deletions

View File

@ -64,7 +64,7 @@ sudo ./zoraxy -port=:8000
## Usage
Zoraxy provides basic authentication system for standalone mode. To use it in standalone mode, follow the instructionss below for your desired deployment platform.
Zoraxy provides basic authentication system for standalone mode. To use it in standalone mode, follow the instructions below for your desired deployment platform.
### Standalone Mode
@ -134,7 +134,7 @@ If you already have an upstream reverse proxy server in place with permission ma
./zoraxy -noauth=true
```
*Note: For security reaons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
*Note: For security reasons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
## Screenshots

View File

@ -38,7 +38,7 @@ func initACME() *acme.ACMEHandler {
port = getRandomPort(30000)
}
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb)
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb, SystemWideLogger)
}
// create the special routing rule for ACME

View File

@ -59,7 +59,7 @@ var enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade
var (
name = "Zoraxy"
version = "3.1.0"
version = "3.1.1"
nodeUUID = "generic" //System uuid, in uuidv4 format
development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix()
@ -117,8 +117,8 @@ func SetupCloseHandler() {
func ShutdownSeq() {
SystemWideLogger.Println("Shutting down " + name)
SystemWideLogger.Println("Closing GeoDB ")
geodbStore.Close()
//SystemWideLogger.Println("Closing GeoDB")
//geodbStore.Close()
SystemWideLogger.Println("Closing Netstats Listener")
netstatBuffers.Close()
SystemWideLogger.Println("Closing Statistic Collector")

View File

@ -11,7 +11,6 @@ import (
"encoding/pem"
"errors"
"fmt"
"log"
"net"
"net/http"
"os"
@ -26,6 +25,7 @@ import (
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
@ -68,25 +68,31 @@ type ACMEHandler struct {
DefaultAcmeServer string
Port string
Database *database.Database
Logger *logger.Logger
}
// NewACME creates a new ACMEHandler instance.
func NewACME(acmeServer string, port string, database *database.Database) *ACMEHandler {
func NewACME(defaultAcmeServer string, port string, database *database.Database, logger *logger.Logger) *ACMEHandler {
return &ACMEHandler{
DefaultAcmeServer: acmeServer,
DefaultAcmeServer: defaultAcmeServer,
Port: port,
Database: database,
Logger: logger,
}
}
func (a *ACMEHandler) Logf(message string, err error) {
a.Logger.PrintAndLog("ACME", message, err)
}
// ObtainCert obtains a certificate for the specified domains.
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool) (bool, error) {
log.Println("[ACME] Obtaining certificate...")
a.Logf("Obtaining certificate for: "+strings.Join(domains, ", "), nil)
// generate private key
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Println(err)
a.Logf("Private key generation failed", err)
return false, err
}
@ -102,7 +108,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
// skip TLS verify if need
// Ref: https://github.com/go-acme/lego/blob/6af2c756ac73a9cb401621afca722d0f4112b1b8/lego/client_config.go#L74
if skipTLS {
log.Println("[INFO] Ignore TLS/SSL Verification Error for ACME Server")
a.Logf("Ignoring TLS/SSL Verification Error for ACME Server", nil)
config.HTTPClient.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
@ -129,16 +135,16 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
// if not custom ACME url, load it from ca.json
if caName == "custom" {
log.Println("[INFO] Using Custom ACME " + caUrl + " for CA Directory URL")
a.Logf("Using Custom ACME "+caUrl+" for CA Directory URL", nil)
} else {
caLinkOverwrite, err := loadCAApiServerFromName(caName)
if err == nil {
config.CADirURL = caLinkOverwrite
log.Println("[INFO] Using " + caLinkOverwrite + " for CA Directory URL")
a.Logf("Using "+caLinkOverwrite+" for CA Directory URL", nil)
} else {
// (caName == "" || caUrl == "") will use default acme
config.CADirURL = a.DefaultAcmeServer
log.Println("[INFO] Using Default ACME " + a.DefaultAcmeServer + " for CA Directory URL")
a.Logf("Using Default ACME "+a.DefaultAcmeServer+" for CA Directory URL", nil)
}
}
@ -146,7 +152,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
client, err := lego.NewClient(config)
if err != nil {
log.Println(err)
a.Logf("Failed to spawn new ACME client from current config", err)
return false, err
}
@ -164,32 +170,32 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
var dnsCredentials string
err := a.Database.Read("acme", certificateName+"_dns_credentials", &dnsCredentials)
if err != nil {
log.Println(err)
a.Logf("Read DNS credential failed", err)
return false, err
}
var dnsProvider string
err = a.Database.Read("acme", certificateName+"_dns_provider", &dnsProvider)
if err != nil {
log.Println(err)
a.Logf("Read DNS Provider failed", err)
return false, err
}
provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials)
if err != nil {
log.Println(err)
a.Logf("Unable to resolve DNS challenge provider", err)
return false, err
}
err = client.Challenge.SetDNS01Provider(provider)
if err != nil {
log.Println(err)
a.Logf("Failed to resolve DNS01 Provider", err)
return false, err
}
} else {
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
if err != nil {
log.Println(err)
a.Logf("Failed to resolve HTTP01 Provider", err)
return false, err
}
}
@ -205,7 +211,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
var reg *registration.Resource
// New users will need to register
if client.GetExternalAccountRequired() {
log.Println("External Account Required for this ACME Provider.")
a.Logf("External Account Required for this ACME Provider", nil)
// IF KID and HmacEncoded is overidden
if !a.Database.TableExists("acme") {
@ -220,20 +226,18 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
var kid string
var hmacEncoded string
err := a.Database.Read("acme", config.CADirURL+"_kid", &kid)
if err != nil {
log.Println(err)
a.Logf("Failed to read kid from database", err)
return false, err
}
err = a.Database.Read("acme", config.CADirURL+"_hmacEncoded", &hmacEncoded)
if err != nil {
log.Println(err)
a.Logf("Failed to read HMAC from database", err)
return false, err
}
log.Println("EAB Credential retrieved.", kid, hmacEncoded)
a.Logf("EAB Credential retrieved: "+kid+" / "+hmacEncoded, nil)
if kid != "" && hmacEncoded != "" {
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
@ -242,14 +246,14 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
})
}
if err != nil {
log.Println(err)
a.Logf("Register with external account binder failed", err)
return false, err
}
//return false, errors.New("External Account Required for this ACME Provider.")
} else {
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
log.Println(err)
a.Logf("Unable to register client", err)
return false, err
}
}
@ -262,7 +266,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
}
certificates, err := client.Certificate.Obtain(request)
if err != nil {
log.Println(err)
a.Logf("Obtain certificate failed", err)
return false, err
}
@ -270,12 +274,12 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
// private key, and a certificate URL.
err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777)
if err != nil {
log.Println(err)
a.Logf("Failed to write public key to disk", err)
return false, err
}
err = os.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777)
if err != nil {
log.Println(err)
a.Logf("Failed to write private key to disk", err)
return false, err
}
@ -289,13 +293,13 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
certInfoBytes, err := json.Marshal(certInfo)
if err != nil {
log.Println(err)
a.Logf("Marshal certificate renew config failed", err)
return false, err
}
err = os.WriteFile("./conf/certs/"+certificateName+".json", certInfoBytes, 0777)
if err != nil {
log.Println(err)
a.Logf("Failed to write certificate renew config to file", err)
return false, err
}
@ -313,7 +317,7 @@ func (a *ACMEHandler) CheckCertificate() []string {
expiredCerts := []string{}
if err != nil {
log.Println(err)
a.Logf("Failed to load certificate folder", err)
return []string{}
}
@ -410,14 +414,14 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
ca, err := utils.PostPara(r, "ca")
if err != nil {
log.Println("[INFO] CA not set. Using default")
a.Logf("CA not set. Using default", nil)
ca, caUrl = "", ""
}
if ca == "custom" {
caUrl, err = utils.PostPara(r, "caURL")
if err != nil {
log.Println("[INFO] Custom CA set but no URL provide, Using default")
a.Logf("Custom CA set but no URL provide, Using default", nil)
ca, caUrl = "", ""
}
}
@ -465,7 +469,7 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
func jsonEscape(i string) string {
b, err := json.Marshal(i)
if err != nil {
log.Println("Unable to escape json data: " + err.Error())
//log.Println("Unable to escape json data: " + err.Error())
return i
}
s := string(b)

View File

@ -75,6 +75,15 @@ func HandleGuidedStepCheck(w http.ResponseWriter, r *http.Request) {
httpServerReachable := isHTTPServerAvailable(domain)
js, _ := json.Marshal(httpServerReachable)
utils.SendJSONResponse(w, string(js))
} else if stepNo == 10 {
//Resolve public Ip address for tour
publicIp, err := getPublicIPAddress()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(publicIp)
utils.SendJSONResponse(w, string(js))
} else {
utils.SendErrorResponse(w, "invalid step number")
}

View File

@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"net/mail"
"os"
@ -12,6 +11,7 @@ import (
"strings"
"time"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
@ -36,6 +36,7 @@ type AutoRenewer struct {
RenewTickInterval int64
EarlyRenewDays int //How many days before cert expire to renew certificate
TickerstopChan chan bool
Logger *logger.Logger //System wide logger
}
type ExpiredCerts struct {
@ -45,7 +46,7 @@ type ExpiredCerts struct {
// Create an auto renew agent, require config filepath and auto scan & renew interval (seconds)
// Set renew check interval to 0 for auto (1 day)
func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64, earlyRenewDays int, AcmeHandler *ACMEHandler) (*AutoRenewer, error) {
func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64, earlyRenewDays int, AcmeHandler *ACMEHandler, logger *logger.Logger) (*AutoRenewer, error) {
if renewCheckInterval == 0 {
renewCheckInterval = 86400 //1 day
}
@ -87,6 +88,7 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
AcmeHandler: AcmeHandler,
RenewerConfig: &renewerConfig,
RenewTickInterval: renewCheckInterval,
Logger: logger,
}
if thisRenewer.RenewerConfig.Enabled {
@ -100,6 +102,10 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
return &thisRenewer, nil
}
func (a *AutoRenewer) Logf(message string, err error) {
a.Logger.PrintAndLog("CertRenew", message, err)
}
func (a *AutoRenewer) StartAutoRenewTicker() {
//Stop the previous ticker if still running
if a.TickerstopChan != nil {
@ -118,7 +124,7 @@ func (a *AutoRenewer) StartAutoRenewTicker() {
case <-done:
return
case <-ticker.C:
log.Println("Check and renew certificates in progress")
a.Logf("Check and renew certificates in progress", nil)
a.CheckAndRenewCertificates()
}
}
@ -233,12 +239,12 @@ func (a *AutoRenewer) HandleAutoRenewEnable(w http.ResponseWriter, r *http.Reque
}
a.RenewerConfig.Enabled = true
a.saveRenewConfigToFile()
log.Println("[ACME] ACME auto renew enabled")
a.Logf("ACME auto renew enabled", nil)
a.StartAutoRenewTicker()
} else {
a.RenewerConfig.Enabled = false
a.saveRenewConfigToFile()
log.Println("[ACME] ACME auto renew disabled")
a.Logf("ACME auto renew disabled", nil)
a.StopAutoRenewTicker()
}
} else {
@ -283,7 +289,7 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
certFolder := a.CertFolder
files, err := os.ReadDir(certFolder)
if err != nil {
log.Println("Unable to renew certificates: " + err.Error())
a.Logf("Read certificate store failed", err)
return []string{}, err
}
@ -303,7 +309,7 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
DNSName, err := ExtractDomains(certBytes)
if err != nil {
//Maybe self signed. Ignore this
log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name())
a.Logf("Encounted error when trying to resolve DNS name for cert "+file.Name(), err)
continue
}
@ -327,11 +333,10 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
}
if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) {
//This cert is expired
DNSName, err := ExtractDomains(certBytes)
if err != nil {
//Maybe self signed. Ignore this
log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name())
a.Logf("Encounted error when trying to resolve DNS name for cert "+file.Name(), err)
continue
}
@ -358,7 +363,7 @@ func (a *AutoRenewer) Close() {
func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, error) {
renewedCertFiles := []string{}
for _, expiredCert := range certs {
log.Println("Renewing " + expiredCert.Filepath + " (Might take a few minutes)")
a.Logf("Renewing "+expiredCert.Filepath+" (Might take a few minutes)", nil)
fileName := filepath.Base(expiredCert.Filepath)
certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
@ -366,10 +371,10 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
certInfoFilename := fmt.Sprintf("%s/%s.json", filepath.Dir(expiredCert.Filepath), certName)
certInfo, err := LoadCertInfoJSON(certInfoFilename)
if err != nil {
log.Printf("Renew %s certificate error, can't get the ACME detail for cert: %v, trying org section as ca", certName, err)
a.Logf("Renew "+certName+"certificate error, can't get the ACME detail for certificate, trying org section as ca", err)
if CAName, extractErr := ExtractIssuerNameFromPEM(expiredCert.Filepath); extractErr != nil {
log.Printf("extract issuer name for cert error: %v, using default ca", extractErr)
a.Logf("Extract issuer name for cert error, using default ca", err)
certInfo = &CertificateInfoJSON{}
} else {
certInfo = &CertificateInfoJSON{AcmeName: CAName}
@ -378,9 +383,9 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS)
if err != nil {
log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error())
a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err)
} else {
log.Println("Successfully renewed " + filepath.Base(expiredCert.Filepath))
a.Logf("Successfully renewed "+filepath.Base(expiredCert.Filepath), nil)
renewedCertFiles = append(renewedCertFiles, filepath.Base(expiredCert.Filepath))
}
}

View File

@ -54,6 +54,9 @@ type ReverseProxy struct {
Prepender string
Verbal bool
//Appended by Zoraxy project
}
type ResponseRewriteRuleSet struct {
@ -350,13 +353,6 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
}
}
//TODO: Figure out a way to proxy for proxmox
//if res.StatusCode == 501 || res.StatusCode == 500 {
// fmt.Println(outreq.Proto, outreq.RemoteAddr, outreq.RequestURI)
// fmt.Println(">>>", outreq.Method, res.Header, res.ContentLength, res.StatusCode)
// fmt.Println(outreq.Header, req.Host)
//}
//Add debug X-Proxy-By tracker
res.Header.Set("x-proxy-by", "zoraxy/"+rrr.Version)

View File

@ -83,6 +83,10 @@ func GetUpstreamsAsString(upstreams []*Upstream) string {
for _, upstream := range upstreams {
targets = append(targets, upstream.String())
}
if len(targets) == 0 {
//No upstream
return "(no upstream config)"
}
return strings.Join(targets, ", ")
}
@ -93,7 +97,7 @@ func (m *RouteManager) Close() {
}
// Print debug message
func (m *RouteManager) debugPrint(message string, err error) {
// Log Println, replace all log.Println or fmt.Println with this
func (m *RouteManager) println(message string, err error) {
m.Options.Logger.PrintAndLog("LoadBalancer", message, err)
}

View File

@ -2,8 +2,6 @@ package loadbalance
import (
"errors"
"fmt"
"log"
"math/rand"
"net/http"
)
@ -29,7 +27,7 @@ func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.R
//No valid session found. Assign a new upstream
targetOrigin, index, err := getRandomUpstreamByWeight(origins)
if err != nil {
fmt.Println("Oops. Unable to get random upstream")
m.println("Unable to get random upstream", err)
targetOrigin = origins[0]
index = 0
}
@ -44,7 +42,7 @@ func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.R
var err error
targetOrigin, _, err = getRandomUpstreamByWeight(origins)
if err != nil {
log.Println(err)
m.println("Failed to get next origin", err)
targetOrigin = origins[0]
}
@ -102,42 +100,66 @@ func (m *RouteManager) getSessionHandler(r *http.Request, upstreams []*Upstream)
/* Functions related to random upstream picking */
// Get a random upstream by the weights defined in Upstream struct, return the upstream, index value and any error
func getRandomUpstreamByWeight(upstreams []*Upstream) (*Upstream, int, error) {
var ret *Upstream
sum := 0
for _, c := range upstreams {
sum += c.Weight
}
r, err := intRange(0, sum)
if err != nil {
return ret, -1, err
}
counter := 0
for _, c := range upstreams {
r -= c.Weight
if r < 0 {
return c, counter, nil
}
counter++
// If there is only one upstream, return it
if len(upstreams) == 1 {
return upstreams[0], 0, nil
}
if ret == nil {
//All fallback
//use the first one that is with weight = 0
fallbackUpstreams := []*Upstream{}
fallbackUpstreamsOriginalID := []int{}
for ix, upstream := range upstreams {
if upstream.Weight == 0 {
fallbackUpstreams = append(fallbackUpstreams, upstream)
fallbackUpstreamsOriginalID = append(fallbackUpstreamsOriginalID, ix)
}
}
upstreamID := rand.Intn(len(fallbackUpstreams))
return fallbackUpstreams[upstreamID], fallbackUpstreamsOriginalID[upstreamID], nil
// Preserve the index with upstreams
type upstreamWithIndex struct {
Upstream *Upstream
Index int
}
return ret, -1, errors.New("failed to pick an upstream origin server")
// Calculate total weight for upstreams with weight > 0
totalWeight := 0
fallbackUpstreams := make([]upstreamWithIndex, 0, len(upstreams))
for index, upstream := range upstreams {
if upstream.Weight > 0 {
totalWeight += upstream.Weight
} else {
// Collect fallback upstreams
fallbackUpstreams = append(fallbackUpstreams, upstreamWithIndex{upstream, index})
}
}
// If there are no upstreams with weight > 0, return a fallback upstream if available
if totalWeight == 0 {
if len(fallbackUpstreams) > 0 {
// Randomly select one of the fallback upstreams
randIndex := rand.Intn(len(fallbackUpstreams))
return fallbackUpstreams[randIndex].Upstream, fallbackUpstreams[randIndex].Index, nil
}
// No upstreams available at all
return nil, -1, errors.New("no valid upstream servers available")
}
// Random weight between 0 and total weight
randomWeight := rand.Intn(totalWeight)
// Select an upstream based on the random weight
for index, upstream := range upstreams {
if upstream.Weight > 0 { // Only consider upstreams with weight > 0
if randomWeight < upstream.Weight {
// Return the selected upstream and its index
return upstream, index, nil
}
randomWeight -= upstream.Weight
}
}
// If we reach here, it means we should return a fallback upstream if available
if len(fallbackUpstreams) > 0 {
randIndex := rand.Intn(len(fallbackUpstreams))
return fallbackUpstreams[randIndex].Upstream, fallbackUpstreams[randIndex].Index, nil
}
return nil, -1, errors.New("failed to pick an upstream origin server")
}
// IntRange returns a random integer in the range from min to max.
/*
func intRange(min, max int) (int, error) {
var result int
switch {
@ -152,3 +174,4 @@ func intRange(min, max int) (int, error) {
}
return result, nil
}
*/

View File

@ -117,7 +117,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(w, r, target.ActiveOrigins, target.UseStickySession)
if err != nil {
http.ServeFile(w, r, "./web/rperror.html")
log.Println(err.Error())
h.Parent.Option.Logger.PrintAndLog("proxy", "Failed to assign an upstream for this request", err)
h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname())
return
}
@ -144,6 +144,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: selectedUpstream.SkipCertValidations,
SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck,
Logger: h.Parent.Option.Logger,
})
wspHandler.ServeHTTP(w, r)
return
@ -177,11 +178,10 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
if err != nil {
if errors.As(err, &dnsError) {
http.ServeFile(w, r, "./web/hosterror.html")
log.Println(err.Error())
h.Parent.logRequest(r, false, 404, "host-http", r.URL.Hostname())
} else {
http.ServeFile(w, r, "./web/rperror.html")
log.Println(err.Error())
//TODO: Take this upstream offline automatically
h.Parent.logRequest(r, false, 521, "host-http", r.URL.Hostname())
}
}
@ -212,6 +212,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: target.SkipCertValidations,
SkipOriginCheck: true, //You should not use websocket via virtual directory. But keep this to true for compatibility
Logger: h.Parent.Option.Logger,
})
wspHandler.ServeHTTP(w, r)
return

View File

@ -70,6 +70,11 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
// Add Proxy Route to current runtime. Call to PrepareProxyRoute before adding to runtime
func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error {
if len(endpoint.ActiveOrigins) == 0 {
//There are no active origins. No need to check for ready
router.ProxyEndpoints.Store(endpoint.RootOrMatchingDomain, endpoint)
return nil
}
if !router.loadBalancer.UpstreamsReady(endpoint.ActiveOrigins) {
//This endpoint is not prepared
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")

View File

@ -18,7 +18,7 @@ func (this *defaultDialer) Dial(address string) Socket {
if socket, err := net.DialTimeout("tcp", address, this.timeout); err == nil {
return socket
} else {
this.logger.Printf("[INFO] Unable to establish connection to [%s]: %s", address, err)
this.logger.Printf("Unable to establish connection to [%s]: %s", address, err)
}
return nil

View File

@ -17,7 +17,7 @@ func (this *loggingInitializer) Initialize(client, server Socket) bool {
result := this.inner.Initialize(client, server)
if !result {
this.logger.Printf("[INFO] Connection failed [%s] -> [%s]", client.RemoteAddr(), server.RemoteAddr())
this.logger.Printf("Connection failed [%s] -> [%s]", client.RemoteAddr(), server.RemoteAddr())
}
return result

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -88,6 +88,7 @@ func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWrite
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: false,
SkipOriginCheck: false,
Logger: nil,
})
wspHandler.ServeHTTP(w, r)
return

View File

@ -6,7 +6,6 @@ import (
"embed"
"encoding/pem"
"io"
"log"
"os"
"path/filepath"
"strings"
@ -185,7 +184,6 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err
//Load the cert and serve it
cer, err := tls.LoadX509KeyPair(pubKey, priKey)
if err != nil {
log.Println(err)
return nil, nil
}

View File

@ -3,7 +3,6 @@ package uptime
import (
"encoding/json"
"errors"
"log"
"net/http"
"net/http/cookiejar"
"strconv"
@ -242,7 +241,7 @@ func getWebsiteStatus(url string) (int, error) {
// Create a one-time use cookie jar to store cookies
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
log.Fatal(err)
return 0, err
}
client := http.Client{

View File

@ -41,12 +41,12 @@ func SendOK(w http.ResponseWriter) {
// Get GET parameter
func GetPara(r *http.Request, key string) (string, error) {
keys, ok := r.URL.Query()[key]
if !ok || len(keys[0]) < 1 {
// Get first value from the URL query
value := r.URL.Query().Get(key)
if len(value) == 0 {
return "", errors.New("invalid " + key + " given")
} else {
return keys[0], nil
}
return value, nil
}
// Get GET paramter as boolean, accept 1 or true
@ -56,26 +56,29 @@ func GetBool(r *http.Request, key string) (bool, error) {
return false, err
}
x = strings.TrimSpace(x)
if x == "1" || strings.ToLower(x) == "true" || strings.ToLower(x) == "on" {
// Convert to lowercase and trim spaces just once to compare
switch strings.ToLower(strings.TrimSpace(x)) {
case "1", "true", "on":
return true, nil
} else if x == "0" || strings.ToLower(x) == "false" || strings.ToLower(x) == "off" {
case "0", "false", "off":
return false, nil
}
return false, errors.New("invalid boolean given")
}
// Get POST paramter
// Get POST parameter
func PostPara(r *http.Request, key string) (string, error) {
r.ParseForm()
x := r.Form.Get(key)
if x == "" {
return "", errors.New("invalid " + key + " given")
} else {
return x, nil
// Try to parse the form
if err := r.ParseForm(); err != nil {
return "", err
}
// Get first value from the form
x := r.Form.Get(key)
if len(x) == 0 {
return "", errors.New("invalid " + key + " given")
}
return x, nil
}
// Get POST paramter as boolean, accept 1 or true
@ -85,11 +88,11 @@ func PostBool(r *http.Request, key string) (bool, error) {
return false, err
}
x = strings.TrimSpace(x)
if x == "1" || strings.ToLower(x) == "true" || strings.ToLower(x) == "on" {
// Convert to lowercase and trim spaces just once to compare
switch strings.ToLower(strings.TrimSpace(x)) {
case "1", "true", "on":
return true, nil
} else if x == "0" || strings.ToLower(x) == "false" || strings.ToLower(x) == "off" {
case "0", "false", "off":
return false, nil
}
@ -114,14 +117,19 @@ func PostInt(r *http.Request, key string) (int, error) {
func FileExists(filename string) bool {
_, err := os.Stat(filename)
if os.IsNotExist(err) {
if err == nil {
// File exists
return true
} else if errors.Is(err, os.ErrNotExist) {
// File does not exist
return false
}
return true
// Some other error
return false
}
func IsDir(path string) bool {
if FileExists(path) == false {
if !FileExists(path) {
return false
}
fi, err := os.Stat(path)
@ -191,4 +199,4 @@ func ValidateListeningAddress(address string) bool {
}
return true
}
}

View File

@ -42,6 +42,12 @@ func (fm *FileManager) HandleList(w http.ResponseWriter, r *http.Request) {
// Construct the absolute path to the target directory
targetDir := filepath.Join(fm.Directory, directory)
// Clean path to prevent path escape #274
targetDir = filepath.ToSlash(filepath.Clean(targetDir))
for strings.Contains(targetDir, "../") {
targetDir = strings.ReplaceAll(targetDir, "../", "")
}
// Open the target directory
dirEntries, err := os.ReadDir(targetDir)
if err != nil {

View File

@ -3,6 +3,7 @@ package websocketproxy
import (
"crypto/tls"
"errors"
"fmt"
"io"
"log"
@ -12,6 +13,7 @@ import (
"strings"
"github.com/gorilla/websocket"
"imuslab.com/zoraxy/mod/info/logger"
)
var (
@ -54,8 +56,9 @@ type WebsocketProxy struct {
// Additional options for websocket proxy runtime
type Options struct {
SkipTLSValidation bool //Skip backend TLS validation
SkipOriginCheck bool //Skip origin check
SkipTLSValidation bool //Skip backend TLS validation
SkipOriginCheck bool //Skip origin check
Logger *logger.Logger //Logger, can be nil
}
// ProxyHandler returns a new http.Handler interface that reverse proxies the
@ -78,17 +81,26 @@ func NewProxy(target *url.URL, options Options) *WebsocketProxy {
return &WebsocketProxy{Backend: backend, Verbal: false, Options: options}
}
// Utilities function for log printing
func (w *WebsocketProxy) Println(messsage string, err error) {
if w.Options.Logger != nil {
w.Options.Logger.PrintAndLog("websocket", messsage, err)
return
}
log.Println("[websocketproxy] [system:info]"+messsage, err)
}
// ServeHTTP implements the http.Handler that proxies WebSocket connections.
func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if w.Backend == nil {
log.Println("websocketproxy: backend function is not defined")
w.Println("Invalid websocket backend configuration", errors.New("backend function not found"))
http.Error(rw, "internal server error (code: 1)", http.StatusInternalServerError)
return
}
backendURL := w.Backend(req)
if backendURL == nil {
log.Println("websocketproxy: backend URL is nil")
w.Println("Invalid websocket backend configuration", errors.New("backend URL is nil"))
http.Error(rw, "internal server error (code: 2)", http.StatusInternalServerError)
return
}
@ -158,13 +170,13 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// http://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-01
connBackend, resp, err := dialer.Dial(backendURL.String(), requestHeader)
if err != nil {
log.Printf("websocketproxy: couldn't dial to remote backend url %s", err)
w.Println("Couldn't dial to remote backend url "+backendURL.String(), err)
if resp != nil {
// If the WebSocket handshake fails, ErrBadHandshake is returned
// along with a non-nil *http.Response so that callers can handle
// redirects, authentication, etcetera.
if err := copyResponse(rw, resp); err != nil {
log.Printf("websocketproxy: couldn't write response after failed remote backend handshake: %s", err)
w.Println("Couldn't write response after failed remote backend handshake to "+backendURL.String(), err)
}
} else {
http.Error(rw, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
@ -198,7 +210,7 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Also pass the header that we gathered from the Dial handshake.
connPub, err := upgrader.Upgrade(rw, req, upgradeHeader)
if err != nil {
log.Printf("websocketproxy: couldn't upgrade %s", err)
w.Println("Couldn't upgrade incoming request", err)
return
}
defer connPub.Close()

View File

@ -31,6 +31,7 @@ func TestProxy(t *testing.T) {
proxy := NewProxy(u, Options{
SkipTLSValidation: false,
SkipOriginCheck: false,
Logger: nil,
})
proxy.Upgrader = upgrader

View File

@ -910,7 +910,6 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
results := []*dynamicproxy.ProxyEndpoint{}
dynamicProxyRouter.ProxyEndpoints.Range(func(key, value interface{}) bool {
thisEndpoint := dynamicproxy.CopyEndpoint(value.(*dynamicproxy.ProxyEndpoint))
//Clear the auth passwords before showing to front-end
cleanedCredentials := []*dynamicproxy.BasicAuthCredentials{}
for _, user := range thisEndpoint.BasicAuthCredentials {
@ -919,7 +918,6 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
PasswordHash: "",
})
}
thisEndpoint.BasicAuthCredentials = cleanedCredentials
results = append(results, thisEndpoint)
return true

View File

@ -285,6 +285,7 @@ func startupSequence() {
int64(*acmeAutoRenewInterval),
*acmeCertAutoRenewDays,
acmeHandler,
SystemWideLogger,
)
if err != nil {
log.Fatal(err)

View File

@ -197,7 +197,7 @@
<div class="item" data-value="lt"><i class="lt flag"></i>Lithuania</div>
<div class="item" data-value="lu"><i class="lu flag"></i>Luxembourg</div>
<div class="item" data-value="mo"><i class="mo flag"></i>Macau</div>
<div class="item" data-value="mk"><i class="mk flag"></i>Macedonia</div>
<div class="item" data-value="mk"><i class="mk flag"></i>North Macedonia</div>
<div class="item" data-value="mg"><i class="mg flag"></i>Madagascar</div>
<div class="item" data-value="mw"><i class="mw flag"></i>Malawi</div>
<div class="item" data-value="my"><i class="my flag"></i>Malaysia</div>
@ -514,7 +514,7 @@
<div class="item" data-value="lt"><i class="lt flag"></i>Lithuania</div>
<div class="item" data-value="lu"><i class="lu flag"></i>Luxembourg</div>
<div class="item" data-value="mo"><i class="mo flag"></i>Macau</div>
<div class="item" data-value="mk"><i class="mk flag"></i>Macedonia</div>
<div class="item" data-value="mk"><i class="mk flag"></i>North Macedonia</div>
<div class="item" data-value="mg"><i class="mg flag"></i>Madagascar</div>
<div class="item" data-value="mw"><i class="mw flag"></i>Malawi</div>
<div class="item" data-value="my"><i class="my flag"></i>Malaysia</div>

View File

@ -59,7 +59,7 @@
</div>
</div>
<p>Current list of loaded certificates</p>
<div>
<div tourstep="certTable">
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table class="ui sortable unstackable basic celled table">
<thead>
@ -79,7 +79,8 @@
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
</div>
<div class="ui divider"></div>
<h3>Fallback Certificate</h3>
<div tourstep="defaultCertificate">
<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>
@ -102,43 +103,46 @@
<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>
<div class="ui divider"></div>
<h3>Certificate Authority (CA) and Auto Renew (ACME)</h3>
<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>
<div class="ui fluid form">
<div class="field">
<label>Preferred CA</label>
<div class="ui selection dropdown" id="defaultCA">
<input type="hidden" name="defaultCA">
<i class="dropdown icon"></i>
<div class="default text">Let's Encrypt</div>
<div class="menu">
<div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
<div class="item" data-value="Buypass">Buypass</div>
<div class="item" data-value="ZeroSSL">ZeroSSL</div>
<div tourstep="acmeSettings">
<h3>Certificate Authority (CA) and Auto Renew (ACME)</h3>
<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>
<div class="ui fluid form">
<div class="field">
<label>Preferred CA</label>
<div class="ui selection dropdown" id="defaultCA">
<input type="hidden" name="defaultCA">
<i class="dropdown icon"></i>
<div class="default text">Let's Encrypt</div>
<div class="menu">
<div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
<div class="item" data-value="Buypass">Buypass</div>
<div class="item" data-value="ZeroSSL">ZeroSSL</div>
</div>
</div>
</div>
</div>
<div class="field">
<label>ACME Email</label>
<input id="prefACMEEmail" type="text" placeholder="ACME Email">
</div>
<button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
</div><br>
<h5>Certificate Renew / Generation (ACME) Settings</h5>
<div class="ui basic segment acmeRenewStateWrapper">
<h4 class="ui header" id="acmeAutoRenewer">
<i class="white remove icon"></i>
<div class="content">
<span id="acmeAutoRenewerStatus">Disabled</span>
<div class="sub header">ACME Auto-Renewer</div>
<div class="field">
<label>ACME Email</label>
<input id="prefACMEEmail" type="text" placeholder="ACME Email">
</div>
</h4>
<button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
</div><br>
<h5>Certificate Renew / Generation (ACME) Settings</h5>
<div class="ui basic segment acmeRenewStateWrapper">
<h4 class="ui header" id="acmeAutoRenewer">
<i class="white remove icon"></i>
<div class="content">
<span id="acmeAutoRenewerStatus">Disabled</span>
<div class="sub header">ACME Auto-Renewer</div>
</div>
</h4>
</div>
<p>This tool provide you a graphical interface to setup auto certificate renew on your (sub)domains. You can also manually generate a certificate if one of your domain do not have certificate.</p>
<button class="ui basic button" tourstep="openACMEManager" onclick="openACMEManager();"><i class="yellow external icon"></i> Open ACME Tool</button>
</div>
<p>This tool provide you a graphical interface to setup auto certificate renew on your (sub)domains. You can also manually generate a certificate if one of your domain do not have certificate.</p>
<button class="ui basic button" onclick="openACMEManager();"><i class="yellow external icon"></i> Open ACME Tool</button>
</div>
<script>
var uploadPendingPublicKey = undefined;

View File

@ -348,6 +348,20 @@
`);
}else if (datatype == "inbound"){
let originalContent = $(column).html();
//Check if this host is covered within one of the certificates. If not, show the icon
let domainIsCovered = true;
let domains = [payload.RootOrMatchingDomain]; //Domain for getting certificate if needed
for (var i = 0; i < payload.MatchingDomainAlias.length; i++){
let thisAliasName = payload.MatchingDomainAlias[i];
domains.push(thisAliasName);
}
if (true){
domainIsCovered = false;
}
//encode the domain to DOM
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
column.empty().append(`${originalContent}
<div class="ui divider"></div>
<div class="ui checkbox" style="margin-top: 0.4em;">
@ -355,10 +369,11 @@
<label>Allow plain HTTP access<br>
<small>Allow inbound connections without TLS/SSL</small></label>
</div><br>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAliasHostnames('${uuid}');"><i class=" blue at icon"></i> Alias</button>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAccessRule('${uuid}');"><i class="ui filter icon"></i> Access Rule</button>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAliasHostnames('${uuid}');"><i class=" blue at icon"></i> Alias</button>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAccessRule('${uuid}');"><i class="ui filter icon"></i> Access Rule</button>
<button class="ui basic compact tiny ${domainIsCovered?"disabled":""} button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="requestCertificateForExistingHost('${uuid}', '${certificateDomains}');"><i class="green lock icon"></i> Get Certificate</button>
`);
$(".hostAccessRuleSelector").dropdown();
}else{
@ -517,6 +532,15 @@
})
}
/*
Certificate Shortcut
*/
function requestCertificateForExistingHost(hostUUID, RootAndAliasDomains){
RootAndAliasDomains = JSON.parse(decodeURIComponent(RootAndAliasDomains))
alert(RootAndAliasDomains.join(", "))
}
//Bind on tab switch events
tabSwitchEventBind["httprp"] = function(){
listProxyEndpoints();

View File

@ -0,0 +1,77 @@
<div id="quickstart" class="standardContainer">
<div class="ui container">
<h1 class="ui header">
<img src="img/res/1F44B.png">
<div class="content" style="font-weight: lighter;">
Welcome to Zoraxy!
<div class="sub header">What services are you planning to setup today?</div>
</div>
</h1>
<br>
<div class="ui stackable equal width grid">
<div class="column">
<div class="serviceOption homepage" name="homepage">
<div class="titleWrapper">
<p>Basic Homepage</p>
</div>
<div class="ui divider"></div>
<p>Host a static homepage with Zoraxy and point your domain name to your web server.</p>
<img class="themebackground ui small image" src="img/res/1F310.png">
<div class="activeOption">
<i class="ui white huge circle check icon"></i>
</div>
</div>
</div>
<div class="column">
<div class="serviceOption subdomain" name="subdomain">
<div class="titleWrapper">
<p>Sub-domains Routing</p>
</div>
<div class="ui divider"></div>
<p>Add and handle traffic from your subdomains and point them to a dedicated web services somewhere else.</p>
<img class="themebackground ui small image" src="img/res/1F500.png">
<div class="activeOption">
<i class="ui white huge circle check icon"></i>
</div>
</div>
</div>
<div class="column">
<div class="serviceOption tls" name="tls">
<div class="titleWrapper">
<p>HTTPS Green Lock(s)</p>
</div>
<div class="ui divider"></div>
<p>Turn your unsafe HTTP website into HTTPS using free certificate from public certificate authorities organizations.</p>
<img class="themebackground ui small image" src="img/res/1F512.png">
<div class="activeOption">
<i class="ui white huge circle check icon"></i>
</div>
</div>
</div>
</div>
<div class="ui divider"></div>
<div style="width: 100%;" align="center">
<button onclick="startQuickStartTour();" class="ui finished button quickstartControlButton">
Start Walkthrough
</button>
</div>
</div>
</div>
<script>
var currentQuickSetupClass = "";
var currentQuickSetupTourStep = 0;
//For tour logic, see quicksetup.js
//Bind selecting events to serviceOption
$("#quickstart .serviceOption").on("click", function(data){
$(".serviceOption.active").removeClass("active");
$(this).addClass("active");
let tourType = $(this).attr("name");
currentQuickSetupClass = tourType;
});
</script>
<script src="script/quicksetup.js"></script>

View File

@ -30,12 +30,12 @@
<h2>New Proxy Rule</h2>
<p>You can add more proxy rules to support more site via domain / subdomains</p>
<div class="ui form">
<div class="field">
<div class="field" tourstep="matchingkeyword">
<label>Matching Keyword / Domain</label>
<input type="text" id="rootname" placeholder="mydomain.com">
<small>Support subdomain and wildcard, e.g. s1.mydomain.com or *.test.mydomain.com. Use comma (,) for alias hostnames. </small>
</div>
<div class="field">
<div class="field" tourstep="targetdomain">
<label>Target IP Address or Domain Name with port</label>
<input type="text" id="proxyDomain" onchange="autoFillTargetTLS(this);">
<small>e.g. 192.168.0.101:8000 or example.com</small>
@ -43,7 +43,7 @@
<div class="field dockerOptimizations" style="display:none;">
<button style="margin-top: -2em;" class="ui basic small button" onclick="openDockerContainersList();"><i class="blue docker icon"></i> Pick from Docker Containers</button>
</div>
<div class="field">
<div class="field" tourstep="requireTLS">
<div class="ui checkbox">
<input type="checkbox" id="reqTls">
<label>Proxy Target require TLS Connection <br><small>(i.e. Your proxy target starts with https://)</small></label>
@ -67,7 +67,7 @@
<i class="ui green lock icon"></i>
Security
</div>
<div class="field">
<div class="field" tourstep="skipTLSValidation">
<div class="ui checkbox">
<input type="checkbox" id="skipTLSValidation">
<label>Ignore TLS/SSL Verification Error<br><small>For targets that is using self-signed, expired certificate (Not Recommended)</small></label>
@ -154,7 +154,9 @@
</div>
</div>
<br>
<button class="ui basic button" onclick="newProxyEndpoint();"><i class="green add icon"></i> Create Endpoint</button>
<div tourstep="newProxyRule" style="display: inline-block;">
<button class="ui basic button" onclick="newProxyEndpoint();"><i class="green add icon"></i> Create Endpoint</button>
</div>
<br><br>
</div>
</div>

View File

@ -53,8 +53,10 @@
</div>
<div class="standardContainer" style="padding-bottom: 0 !important;">
<!-- Power Buttons-->
<button id="startbtn" class="ui basic button" onclick="startService();"><i class="ui green arrow alternate circle up icon"></i> Start Service</button>
<button id="stopbtn" class="ui basic notloopbackOnly disabled button" onclick="stopService();"><i class="ui red minus circle icon"></i> Stop Service</button>
<div class="poweroptions" style="display:inline-block;">
<button id="startbtn" class="ui basic button" onclick="startService();"><i class="ui green arrow alternate circle up icon"></i> Start Service</button>
<button id="stopbtn" class="ui basic notloopbackOnly disabled button" onclick="stopService();"><i class="ui red minus circle icon"></i> Stop Service</button>
</div>
<div class="ui divider"></div>
<h4>Network Status</h4>
<p>Overall Network I/O in Current Host Server</p>
@ -69,7 +71,7 @@
<div class="ui divider"></div>
<h4>Global Settings</h4>
<p>Inbound Port (Reverse Proxy Listening Port)</p>
<div class="ui action fluid notloopbackOnly input">
<div class="ui action fluid notloopbackOnly input" tourstep="incomingPort">
<small id="applyButtonReminder">Click "Apply" button to confirm listening port changes</small>
<input type="text" id="incomingPort" placeholder="Incoming Port" value="80">
<button class="ui green notloopbackOnly button" style="background: linear-gradient(60deg, #27e7ff, #00ca52);" onclick="handlePortChange();"><i class="ui checkmark icon"></i> Apply</button>
@ -86,9 +88,11 @@
<small>(Only apply when TLS enabled and not using port 80)</small></label>
</div>
<br>
<div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em; padding-left: 2em;">
<input type="checkbox">
<label>Force redirect HTTP request to HTTPS</label>
<div tourstep="forceHttpsRedirect" style="display: inline-block;">
<div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em; padding-left: 2em;">
<input type="checkbox">
<label>Force redirect HTTP request to HTTPS</label>
</div>
</div>
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div class="ui accordion advanceSettings">

View File

@ -13,34 +13,35 @@
</div>
</h4>
</div>
<h3>Web Server Settings</h3>
<div class="ui form">
<div class="inline field">
<div class="ui toggle checkbox webservRootDisabled">
<input id="webserv_enable" type="checkbox" class="hidden">
<label>Enable Static Web Server</label>
<div>
<h3>Web Server Settings</h3>
<div class="ui form">
<div class="inline field">
<div class="ui toggle checkbox webservRootDisabled">
<input id="webserv_enable" type="checkbox" class="hidden">
<label>Enable Static Web Server</label>
</div>
</div>
</div>
<div class="inline field">
<div class="ui toggle checkbox">
<input id="webserv_enableDirList" type="checkbox" class="hidden">
<label>Enable Directory Listing</label>
<small>If this folder do not contains any index files, list the directory of this folder.</small>
<div class="inline field">
<div class="ui toggle checkbox">
<input id="webserv_enableDirList" type="checkbox" class="hidden">
<label>Enable Directory Listing</label>
<small>If this folder do not contains any index files, list the directory of this folder.</small>
</div>
</div>
<div class="field">
<label>Document Root Folder</label>
<input id="webserv_docRoot" type="text" readonly="true">
<small>
The web server root folder can only be changed via startup flags of zoraxy for security reasons.
See the -webserv flag for more details.
</small>
</div>
<div class="field webservRootDisabled">
<label>Port Number</label>
<input id="webserv_listenPort" type="number" step="1" min="0" max="65535" value="8081" onchange="updateWebServLinkExample(this.value);">
<small>Use <code>http://127.0.0.1:<span class="webserv_port">8081</span></code> in proxy rules to access the web server</small>
</div>
</div>
<div class="field">
<label>Document Root Folder</label>
<input id="webserv_docRoot" type="text" readonly="true">
<small>
The web server root folder can only be changed via startup flags of zoraxy for security reasons.
See the -webserv flag for more details.
</small>
</div>
<div class="field webservRootDisabled">
<label>Port Number</label>
<input id="webserv_listenPort" type="number" step="1" min="0" max="65535" value="8081" onchange="updateWebServLinkExample(this.value);">
<small>Use <code>http://127.0.0.1:<span class="webserv_port">8081</span></code> in proxy rules to access the web server</small>
</div>
</div>
<small><i class="ui blue save icon"></i> Changes are saved automatically</small>

BIN
src/web/img/res/1F310.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/web/img/res/1F387.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
src/web/img/res/1F38A.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
src/web/img/res/1F44B.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/web/img/res/1F500.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
src/web/img/res/1F512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
src/web/img/res/1F914.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
src/web/img/res/2728.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
src/web/img/res/2753.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
src/web/img/res/E25E.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -36,6 +36,9 @@
<div class="wrapper">
<div class="toolbar">
<div id="mainmenu" class="ui secondary vertical menu">
<a class="item" tag="qstart">
<i class="simplistic magic icon"></i>Quick Start
</a>
<a class="item active" tag="status">
<i class="simplistic info circle icon"></i>Status
</a>
@ -92,6 +95,9 @@
</div>
</div>
<div class="contentWindow">
<!-- Quick Start -->
<div id="qstart" class="functiontab" target="quickstart.html"></div>
<!-- Status Tab -->
<div id="status" class="functiontab" target="status.html" style="display: block ;">
<br><br><div class="ui active centered inline loader"></div>
@ -171,6 +177,22 @@
<div class="questionToConfirm">Confirm Exit?</div>
</div>
</div>
<div id="tourModal" class="nofocus" position="center">
<h4 class="tourStepTitle">Welcome to Zoraxy Tour</h4>
<p class="tourStepContent">This is a simplified tour to show some of what it can do.
Use your keyboard or click the next button to get going.</p>
<div class="ui divider"></div>
<div class="ui equal width grid" align="center">
<div class="column"><button onclick="previousTourStep();" class="ui basic small disabled button tourStepButtonBack">Back</button></div>
<div class="column"><p style="margin-top: 0.4em">Steps <span class="tourStepCounter">1 / 9</span></p></div>
<div class="column nextStepAvaible"><button onclick="nextTourStep();" class="ui basic right floated small button tourStepButtonNext">Next</button></div>
<div class="column nextStepFinish"><button onclick="endTourFocus();" class="ui right floated small button tourStepButtonFinish">Finish</button></div>
</div>
<button onclick="endTourFocus();" class="ui circular small icon button tourCloseButton"><i class="ui times icon"></i></button>
</div>
<div id="tourModalOverlay" style="display:none;"></div>
<br><br>
<script>
$(".year").text(new Date().getFullYear());

View File

@ -46,7 +46,7 @@ body.darkTheme{
--button_border_color: #646464;
}
/* Theme Toggle Css */
/* Theme Toggle CSS */
#themeColorButton{
background-color: black;
color: var(--text_color_inverted);
@ -85,7 +85,6 @@ body{
top: 0;
width: 100%;
z-index: 10;
}
.menubar .logo{
@ -154,7 +153,7 @@ body{
right: 1em;
display:none;
max-width: 300px;
z-index: 999;
z-index: 1000;
}
/* Confirm Box */
@ -519,6 +518,14 @@ body{
display:none;
}
/*
Default Site
*/
#setroot{
border-radius: 0.6em;
}
/*
HTTP Proxy & Virtual Directory
*/
@ -710,4 +717,153 @@ body{
#traceroute_results::selection {
background: #a9d1f3;
}
}
/*
Quick Start Overview
*/
#quickstart .serviceOption{
position: relative;
overflow: hidden;
padding: 1em;
background-color: rgb(240, 240, 240);
border-radius: 0.6em;
cursor: pointer;
min-height: 250px;
transition: opacity 0.1s ease-in-out;
}
#quickstart .serviceOption .activeOption{
position: absolute;
bottom: 0.2em;
left: 0.2em;
display:none;
}
#quickstart .serviceOption.active .activeOption{
display: block;
}
#quickstart .serviceOption .titleWrapper{
text-align: center;
width: 100%;
font-weight: bolder;
font-size: 1.3em;
}
#quickstart .serviceOption :not(.titleWrapper){
font-weight: bold;
}
#quickstart .serviceOption .themebackground{
opacity: 0.2;
position: absolute;
right: 0;
bottom: 0;
margin-right: -1em;
margin-bottom: -2em;
pointer-events: none;
user-select: none;
}
#quickstart .serviceOption:not(.active):hover{
opacity: 0.8;
}
#quickstart .serviceOption.tls{
background: var(--theme_green);
color: white;
}
#quickstart .serviceOption.subdomain{
background: var(--theme_background);
color: white;
}
#quickstart .serviceOption.homepage{
background: var(--theme_background_inverted);
color: white;
}
#quickstart .finished.ui.button{
background: var(--theme_green);
color: white;
}
#tourModal{
background-color: white;
border-radius: 0.6em;
padding: 1.4em;
position: fixed;
z-index: 999;
width: 380px;
display:none;
border: 1px solid rgb(230, 230, 230);
box-shadow: 3px 3px 11px -3px rgba(0,0,0,0.3);
}
/* Locations of tourModal */
#tourModal[position="center"]{
top: 200px;
left: calc(50% - 190px);
}
#tourModal[position="topleft"]{
top: 4em;
left: 4em;
}
#tourModal[position="topright"]{
top: 4em;
right: 4em;
}
#tourModal[position="bottomleft"]{
bottom: 4em;
left: 4em;
}
#tourModal[position="bottomright"]{
bottom: 4em;
right: 4em;
}
#tourModal .tourStepButtonFinish{
background: var(--theme_green) !important;
color: white;
}
#tourModal .tourCloseButton{
position: absolute;
top: 0em;
right: 0em;
margin-top: -0.6em;
margin-right: -0.6em;
}
#tourModal .nextStepFinish{
display: none;
}
#tourModal.nofocus{
box-shadow: 0 0 0 max(100vh, 100vw) rgba(0, 0, 0, .3);
}
#tourModalOverlay{
position: fixed;
top: 10em;
left: 10em;
width: 300px;
height: 200px;
pointer-events: none;
z-index: 199;
border-radius: 0.6em;
box-shadow: 0 0 0 max(100vh, 100vw) rgba(0, 0, 0, .3);
}

View File

@ -0,0 +1,506 @@
/*
Quick Setup Tour
This script file contains all the required script
for quick setup tour and walkthrough
*/
//tourStepFactory generate a function that renders the steps in tourModal
//Keys: {element, title, desc, tab, pos, scrollto, callback}
// elements -> Element (selector) to focus on
// tab -> Tab ID to switch pages
// pos -> Where to display the tour modal, {topleft, topright, bottomleft, bottomright, center}
// scrollto -> Element (selector) to scroll to, can be different from elements
// ignoreVisiableCheck -> Force highlight even if element is currently not visable
function adjustTourModalOverlayToElement(element){;
if ($(element) == undefined || $(element).offset() == undefined){
return;
}
let padding = 12;
$("#tourModalOverlay").css({
"top": $(element).offset().top - padding - $(document).scrollTop(),
"left": $(element).offset().left - padding,
"width": $(element).width() + 2 * padding,
"height": $(element).height() + 2 * padding,
});
}
var tourOverlayUpdateTicker;
function tourStepFactory(config){
return function(){
//Check if this step require tab swap
if (config.tab != undefined && config.tab != ""){
//This tour require tab swap. call to openTabById
openTabById(config.tab);
}
if (config.ignoreVisiableCheck == undefined){
config.ignoreVisiableCheck = false;
}
if (config.element == undefined || (!$(config.element).is(":visible") && !config.ignoreVisiableCheck)){
//No focused element in this step.
$(".tourFocusObject").removeClass("tourFocusObject");
$("#tourModal").addClass("nofocus");
$("#tourModalOverlay").hide();
//If there is a target element to scroll to
if (config.scrollto != undefined){
$('html, body').animate({
scrollTop: $(config.scrollto).offset().top - 100
}, 500);
}
}else{
let elementHighligher = function(){
//Match the overlay to element position and size
$(window).off("resize").on("resize", function(){
adjustTourModalOverlayToElement(config.element);
});
if (tourOverlayUpdateTicker != undefined){
clearInterval(tourOverlayUpdateTicker);
}
tourOverlayUpdateTicker = setInterval(function(){
adjustTourModalOverlayToElement(config.element);
}, 500);
adjustTourModalOverlayToElement(config.element);
$("#tourModalOverlay").fadeIn();
}
//Consists of focus element in this step
$(".tourFocusObject").removeClass("tourFocusObject");
$(config.element).addClass("tourFocusObject");
$("#tourModal").removeClass("nofocus");
$("#tourModalOverlay").hide();
//If there is a target element to scroll to
if (config.scrollto != undefined){
$('html, body').animate({
scrollTop: $(config.scrollto).offset().top - 100
}, 300, function(){
setTimeout(elementHighligher, 300);
});
}else{
setTimeout(elementHighligher, 300);
}
}
//Get the modal location of this step
let showupZone = "center";
if (config.pos != undefined){
showupZone = config.pos
}
$("#tourModal").attr("position", showupZone);
$("#tourModal .tourStepTitle").html(config.title);
$("#tourModal .tourStepContent").html(config.desc);
if (config.callback != undefined){
config.callback();
}
}
}
//Hide the side warpper in tour mode and prevent body from restoring to
//overflow scroll mode
function hideSideWrapperInTourMode(){
hideSideWrapper(); //Call to index.html hide side wrapper function
$("body").css("overflow", "hidden"); //Restore overflow state
}
function startQuickStartTour(){
if (currentQuickSetupClass == ""){
msgbox("No selected setup service tour", false);
return;
}
//Show the tour modal
$("#tourModal").show();
//Load the tour steps
if (tourSteps[currentQuickSetupClass] == undefined || tourSteps[currentQuickSetupClass].length == 0){
//This tour is not defined or empty
let notFound = tourStepFactory({
title: "😭 Tour not found",
desc: "Seems you are requesting a tour that has not been developed yet. Check back on later!"
});
notFound();
//Enable the finish button
$("#tourModal .nextStepAvaible").hide();
$("#tourModal .nextStepFinish").show();
//Set step counter to 1
$("#tourModal .tourStepCounter").text("0 / 0");
return;
}else{
tourSteps[currentQuickSetupClass][0]();
}
updateTourStepCount();
//Disable the previous button
if (tourSteps[currentQuickSetupClass].length == 1){
//There are only 1 step in this tour
$("#tourModal .nextStepAvaible").hide();
$("#tourModal .nextStepFinish").show();
}else{
$("#tourModal .nextStepAvaible").show();
$("#tourModal .nextStepFinish").hide();
}
$("#tourModal .tourStepButtonBack").addClass("disabled");
//Disable body scroll and let tour steps to handle scrolling
$("body").css("overflow-y","hidden");
$("#mainmenu").css("pointer-events", "none");
}
function updateTourStepCount(){
let tourlistLength = tourSteps[currentQuickSetupClass]==undefined?1:tourSteps[currentQuickSetupClass].length;
$("#tourModal .tourStepCounter").text((currentQuickSetupTourStep + 1) + " / " + tourlistLength);
}
function nextTourStep(){
//Add one to the tour steps
currentQuickSetupTourStep++;
if (currentQuickSetupTourStep == tourSteps[currentQuickSetupClass].length - 1){
//Already the last step
$("#tourModal .nextStepAvaible").hide();
$("#tourModal .nextStepFinish").show();
}
updateTourStepCount();
tourSteps[currentQuickSetupClass][currentQuickSetupTourStep]();
if (currentQuickSetupTourStep > 0){
$("#tourModal .tourStepButtonBack").removeClass("disabled");
}
}
function previousTourStep(){
if (currentQuickSetupTourStep > 0){
currentQuickSetupTourStep--;
}
if (currentQuickSetupTourStep != tourSteps[currentQuickSetupClass].length - 1){
//Not at the last step
$("#tourModal .nextStepAvaible").show();
$("#tourModal .nextStepFinish").hide();
}
if (currentQuickSetupTourStep == 0){
//Cant go back anymore
$("#tourModal .tourStepButtonBack").addClass("disabled");
}
updateTourStepCount();
tourSteps[currentQuickSetupClass][currentQuickSetupTourStep]();
}
//End tour and reset everything
function endTourFocus(){
$(".tourFocusObject").removeClass("tourFocusObject");
$(".serviceOption.active").removeClass("active");
currentQuickSetupClass = "";
currentQuickSetupTourStep = 0;
$("#tourModal").hide();
$("#tourModal .nextStepAvaible").show();
$("#tourModal .nextStepFinish").hide();
$("#tourModalOverlay").hide();
if (tourOverlayUpdateTicker != undefined){
clearInterval(tourOverlayUpdateTicker);
}
$("body").css("overflow-y","auto");
$("#mainmenu").css("pointer-events", "auto");
}
var tourSteps = {
//Homepage steps
"homepage": [
tourStepFactory({
title: "🎉 Congratulation on your first site!",
desc: "In this tour, you will be guided through the steps required to setup a basic static website using your own domain name with Zoraxy."
}),
tourStepFactory({
title: "👉 Pointing domain DNS to Zoraxy's IP",
desc: `Setup a DNS A Record that points your domain name to this Zoraxy instances public IP address. <br>
Assume your public IP is 93.184.215.14, you should have an A record like this.
<table class="ui celled collapsing basic striped table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>yourdomain.com</td>
<td>A</td>
<td>93.184.215.14</td>
</tr>
</tbody>
</table>
<br>If the IP of Zoraxy start from 192.168, you might want to use your router's public IP address and setup port forward for both port 80 and 443 as well.`,
callback: function(){
$.get("/api/acme/wizard?step=10", function(data){
if (data.error == undefined){
//Should return the public IP address from acme wizard
//Overwrite the sample IP address
let originalText = $("#tourModal .tourStepContent").html();
originalText = originalText.split("93.184.215.14").join(data);
$("#tourModal .tourStepContent").html(originalText);
}
})
}
}),
tourStepFactory({
title: "🏠 Setup Default Site",
desc: `If you already have an apache or nginx web server running, use "Reverse Proxy Target" and enter your current web server IP address. <br>Otherwise, pick "Internal Static Web Server" and click "Apply Change"`,
tab: "setroot",
element: "#setroot",
pos: "bottomright"
}),
tourStepFactory({
title: "🌐 Enable Static Web Server",
desc: `Enable the static web server if it is not already enabled. Skip this step if you are using external web servers like Apache or Nginx.`,
tab: "webserv",
element: "#webserv",
pos: "bottomright"
}),
tourStepFactory({
title: "📤 Upload Static Website",
desc: `Upload your static website files (e.g. HTML files) to the web directory. If remote access is not avaible, you can also upload it with the web server file manager here.`,
tab: "webserv",
element: "#webserv_dirManager",
pos: "bottomright",
scrollto: "#webserv_dirManager"
}),
tourStepFactory({
title: "💡 Start Zoraxy HTTP listener",
desc: `Start Zoraxy (if it is not already running) by pressing the "Start Service" button.<br>You should now be able to visit your domain and see the static web server contents show up in your browser.`,
tab: "status",
element: "#status .poweroptions",
pos: "bottomright",
})
],
//Subdomains tour steps
"subdomain":[
tourStepFactory({
title: "🎉 Creating your first subdomain",
desc: "Seems you are now ready to expand your site with more services! To do so, you can create a new subdomain for your new web services. <br><br>In this tour, you will be guided through the steps to setup a new subdomain reverse proxy.",
pos: "center"
}),
tourStepFactory({
title: "👉 Pointing subdomain DNS to Zoraxy's IP",
desc: `Setup a DNS CNAME Record that points your subdomain to your root domain. <br>
Assume your public IP is 93.184.215.14, you should have an CNAME record like this.
<table class="ui celled collapsing basic striped table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>example.com</td>
<td>A</td>
<td>93.184.215.14</td>
</tr>
<tr>
<td>sub.example.com</td>
<td>CNAME</td>
<td>example.com</td>
</tr>
</tbody>
</table>`,
callback: function(){
$.get("/api/acme/wizard?step=10", function(data){
if (data.error == undefined){
//Should return the public IP address from acme wizard
//Overwrite the sample IP address
let originalText = $("#tourModal .tourStepContent").html();
originalText = originalText.split("93.184.215.14").join(data);
$("#tourModal .tourStepContent").html(originalText);
}
})
}
}),
tourStepFactory({
title: " Create New Proxy Rule",
desc: `Next, you can now move on to create a proxy rule that reverse proxy your new subdomain in Zoraxy. You can easily add new rules using the "New Proxy Rule" web form.`,
tab: "rules",
pos: "topright"
}),
tourStepFactory({
title: "🌐 Matching Keyword / Domain",
desc: `Fill in your new subdomain in the "Matching Keyword / Domain" field.<br> e.g. sub.example.com`,
element: "#rules .field[tourstep='matchingkeyword']",
pos: "bottomright"
}),
tourStepFactory({
title: "🖥️ Target IP Address or Domain Name with port",
desc: `Fill in the Reverse Proxy Destination. e.g. localhost:8080 or 192.168.1.100:9096. <br><br>Please make sure your web services is accessible by Zoraxy.`,
element: "#rules .field[tourstep='targetdomain']",
pos: "bottomright"
}),
tourStepFactory({
title: "🔐 Proxy Target require TLS Connection",
desc: `If your upstream service only accept https connection, select this option.`,
element: "#rules .field[tourstep='requireTLS']",
pos: "bottomright",
}),
tourStepFactory({
title: "🔓 Ignore TLS Validation Error",
desc: `Some open source projects like Proxmox or NextCloud use self-signed certificate to serve its web UI. If you are proxying services like that, enable this option. `,
element: "#rules #advanceProxyRules .field[tourstep='skipTLSValidation']",
scrollto: "#rules #advanceProxyRules",
pos: "bottomright",
ignoreVisiableCheck: true,
callback: function(){
$("#advanceProxyRules").accordion();
if (!$("#rules #advanceProxyRules .content").is(":visible")){
//Open up the advance config menu
$("#rules #advanceProxyRules .title")[0].click()
}
}
}),
tourStepFactory({
title: "💾 Save New Proxy Rule",
desc: `Now, click "Create Endpoint" to add this reverse proxy rule to runtime.`,
element: "#rules div[tourstep='newProxyRule']",
scrollto: "#rules div[tourstep='newProxyRule']",
pos: "topright",
}),
tourStepFactory({
title: "🎉 New Proxy Rule Setup Completed!",
desc: `You can continue to add more subdomains or alias domain using this web form. To view the created reverse proxy rules, you can navigate to the HTTP Proxy tab.`,
element: "#rules",
tab: "rules",
pos: "bottomright",
}),
tourStepFactory({
title: "🌲 HTTP Proxy List",
desc: `In this tab, you will see all the created HTTP proxy rules and edit them if needed. You should see your newly created HTTP proxy rule in the above list. <Br><Br>
This is the end of this tour. If you want further documentation on how to setup access control filters or load balancer, check out our Github Wiki page.`,
element: "#httprp",
tab: "httprp",
pos: "bottomright",
}),
],
//TLS and ACME tour steps
"tls":[
tourStepFactory({
title: "🔐 Enable HTTPS (TLS) for your site",
desc: `Some technologies only work with HTTPS for security reasons. In this tour, you will be guided through the steps to enable HTTPS in Zoraxy.`,
pos: "center",
}),
tourStepFactory({
title: "➡️ Change Listening Port",
desc: `HTTPS listen on port 443 instead of 80. If your Zoraxy is currently listening to ports other than 443, change it to 443 in incoming port option and click "Apply"`,
tab: "status",
element: "#status div[tourstep='incomingPort']",
scrollto: "#status div[tourstep='incomingPort']",
pos: "bottomright",
}),
tourStepFactory({
title: "🔑 Enable TLS Serving",
desc: `Next, you can enable TLS by checking the "Use TLS to serve proxy request"`,
element: "#tls",
scrollto: "#tls",
pos: "bottomright",
}),
tourStepFactory({
title: "💻 Enable HTTP Server on Port 80",
desc: `As we might want some proxy rules to be accessible by HTTP, turn on the HTTP server listener on port 80 as well.`,
element: "#listenP80",
scrollto: "#tls",
pos: "bottomright",
}),
tourStepFactory({
title: "↩️ Force redirect HTTP request to HTTPS",
desc: `By default, if a HTTP host-name is not found, 404 error page will be returned. However, in common scenerio for self-hosting, you might want to redirect that request to your HTTPS server instead. <br><br>Enabling this option allows such redirection to be done automatically.`,
element: "#status div[tourstep='forceHttpsRedirect']",
scrollto: "#tls",
pos: "bottomright",
}),
tourStepFactory({
title: "🎉 HTTPS Enabled!",
desc: `Now, your Zoraxy instance is ready to serve HTTPS requests.
<br><br>By default, Zoraxy serve all your host-names by its internal self-signed certificate which is not a proper setup. That is why you will need to request a proper certificate for your site from your ISP or CA. `,
tab: "status",
pos: "center",
}),
tourStepFactory({
title: "🔐 TLS / SSL Certificates",
desc: `Zoraxy come with a simple and handy TLS management interface, where you can upload or request your certificates with a web form. You can click "TLS / SSL Certificate" from the side menu to open this page.`,
tab: "cert",
element: "#mainmenu",
pos: "center",
}),
tourStepFactory({
title: "📤 Uploading Fallback (Default) Certificate",
desc: `If you are using Cloudflare, you can upload the Cloudflare (full) strict mode certificate in the "Fallback Certificate" section and let Cloudflare handle all the remaining certificate dispatch. <br><br>
Public key usually use a file extension of .pub or .pem, and private key usually ends with .key.`,
element: "#cert div[tourstep='defaultCertificate']",
scrollto: "#cert div[tourstep='defaultCertificate']",
pos: "bottomright",
}),
tourStepFactory({
title: "⚙️ Setup ACME",
desc: `If you didn't want to pay for a certificate, there are free CA where you can use to obtain a certificate. By default, Let's Encrypt is used and in order to use their service, you will need to fill in your webmin contact email in the "ACME EMAIL" field.
<br><br> After you are done, click "Save Settings" and continue.`,
element: "#cert div[tourstep='acmeSettings']",
scrollto: "#cert div[tourstep='acmeSettings']",
pos: "bottomright",
}),
tourStepFactory({
title: "👉 Open ACME Tool",
desc: `Open the ACME Tool by pressing the button below the ACME settings. You will see a tool window popup from the side.`,
element: ".sideWrapper",
pos: "center",
callback: function(){
//Call to function in cert.html
openACMEManager();
}
}),
tourStepFactory({
title: "📃 Obtain Certificate with ACME",
desc: `Now, we can finally start requesting a free certificate from the selected CA. Fill in the "Generate New Certificate" web-form and click <b>"Get Certificate"</b>.
This usually will takes a few minutes. Wait until the spinning icon disappear before moving on the next step.
<br><br>Tips: You can check the "Use DNS Challenge" if you are trying to request a certificate containing wildcard character (*).`,
element: ".sideWrapper",
pos: "topleft",
}),
tourStepFactory({
title: "🔄 Enable Auto Renew",
desc:`Free certificate only last for a few months. If you want Zoraxy to help you automate the certificate renew process, enable "Auto Renew" by clicking the <b>"Enable Certificate Auto Renew"</b> toggle switch.
<br><br>You can fine tune which certificate to renew in the "Advance Renew Policy" dropdown.`,
element: ".sideWrapper",
pos: "bottomleft",
callback: function(){
//If the user arrive this step from "Back"
if (!$(".sideWrapper").is(":visible")){
openACMEManager();
}
}
}),
tourStepFactory({
title: "🎉 Certificate Installed!",
desc:`Now, your certificate is loaded into the database and it is ready to use! In Zoraxy, you do not need to manually assign the certificate to a domain. Zoraxy will do that automatically for you.
<br><br>Now, you can try to visit your website with https:// and see your green lock shows up next to your domain name!`,
element: "#cert div[tourstep='certTable']",
scrollto: "#cert div[tourstep='certTable']",
pos: "bottomright",
callback: function(){
hideSideWrapperInTourMode();
}
}),
],
}

View File

@ -30,7 +30,7 @@ Object.defineProperty(String.prototype, 'capitalize', {
//Add a new function to jquery for ajax override with csrf token injected
$.cjax = function(payload){
let requireTokenMethod = ["POST", "PUT", "DELETE"];;
let requireTokenMethod = ["POST", "PUT", "DELETE"];
if (requireTokenMethod.includes(payload.method) || requireTokenMethod.includes(payload.type)){
//csrf token is required
let csrfToken = document.getElementsByTagName("meta")["zoraxy.csrf.Token"].getAttribute("content");