@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -31,6 +31,7 @@ func TestProxy(t *testing.T) {
|
||||
proxy := NewProxy(u, Options{
|
||||
SkipTLSValidation: false,
|
||||
SkipOriginCheck: false,
|
||||
Logger: nil,
|
||||
})
|
||||
proxy.Upgrader = upgrader
|
||||
|
||||
|
@ -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
|
||||
|
@ -285,6 +285,7 @@ func startupSequence() {
|
||||
int64(*acmeAutoRenewInterval),
|
||||
*acmeCertAutoRenewDays,
|
||||
acmeHandler,
|
||||
SystemWideLogger,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
77
src/web/components/quickstart.html
Normal 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>
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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
After Width: | Height: | Size: 14 KiB |
BIN
src/web/img/res/1F387.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/web/img/res/1F38A.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/web/img/res/1F44B.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/web/img/res/1F500.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
src/web/img/res/1F512.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/web/img/res/1F914.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/web/img/res/2728.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/web/img/res/2753.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
src/web/img/res/E25E.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
@ -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());
|
||||
|
164
src/web/main.css
@ -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);
|
||||
}
|
506
src/web/script/quicksetup.js
Normal 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();
|
||||
}
|
||||
}),
|
||||
|
||||
],
|
||||
}
|
@ -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");
|
||||
|