diff --git a/src/api.go b/src/api.go index 59c5df4..17fd3c4 100644 --- a/src/api.go +++ b/src/api.go @@ -72,15 +72,20 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) { // Register the APIs for TLS / SSL certificate management functions func RegisterTLSAPIs(authRouter *auth.RouterDef) { + //Global certificate settings authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy) authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest) - authRouter.HandleFunc("/api/cert/upload", handleCertUpload) - authRouter.HandleFunc("/api/cert/download", handleCertDownload) - authRouter.HandleFunc("/api/cert/list", handleListCertificate) - authRouter.HandleFunc("/api/cert/listdomains", handleListDomains) - authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck) - authRouter.HandleFunc("/api/cert/delete", handleCertRemove) authRouter.HandleFunc("/api/cert/resolve", handleCertTryResolve) + authRouter.HandleFunc("/api/cert/setPreferredCertificate", handleSetDomainPreferredCertificate) + + //Certificate store functions + authRouter.HandleFunc("/api/cert/upload", tlsCertManager.HandleCertUpload) + authRouter.HandleFunc("/api/cert/download", tlsCertManager.HandleCertDownload) + authRouter.HandleFunc("/api/cert/list", tlsCertManager.HandleListCertificate) + authRouter.HandleFunc("/api/cert/listdomains", tlsCertManager.HandleListDomains) + authRouter.HandleFunc("/api/cert/checkDefault", tlsCertManager.HandleDefaultCertCheck) + authRouter.HandleFunc("/api/cert/delete", tlsCertManager.HandleCertRemove) + authRouter.HandleFunc("/api/cert/selfsign", tlsCertManager.HandleSelfSignCertGenerate) } // Register the APIs for Authentication handlers like Forward Auth and OAUTH2 diff --git a/src/cert.go b/src/cert.go index 8d2a4ed..62b4f66 100644 --- a/src/cert.go +++ b/src/cert.go @@ -1,188 +1,14 @@ package main import ( - "crypto/x509" "encoding/json" - "encoding/pem" - "fmt" - "io" "net/http" - "os" "path/filepath" - "sort" "strings" - "time" - "imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/utils" ) -// Check if the default certificates is correctly setup -func handleDefaultCertCheck(w http.ResponseWriter, r *http.Request) { - type CheckResult struct { - DefaultPubExists bool - DefaultPriExists bool - } - - pub, pri := tlsCertManager.DefaultCertExistsSep() - js, _ := json.Marshal(CheckResult{ - pub, - pri, - }) - - utils.SendJSONResponse(w, string(js)) -} - -// Return a list of domains where the certificates covers -func handleListCertificate(w http.ResponseWriter, r *http.Request) { - filenames, err := tlsCertManager.ListCertDomains() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - showDate, _ := utils.GetPara(r, "date") - if showDate == "true" { - type CertInfo struct { - Domain string - LastModifiedDate string - ExpireDate string - RemainingDays int - UseDNS bool - } - - results := []*CertInfo{} - - for _, filename := range filenames { - certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".pem") - //keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key") - fileInfo, err := os.Stat(certFilepath) - if err != nil { - utils.SendErrorResponse(w, "invalid domain certificate discovered: "+filename) - return - } - modifiedTime := fileInfo.ModTime().Format("2006-01-02 15:04:05") - - certExpireTime := "Unknown" - certBtyes, err := os.ReadFile(certFilepath) - expiredIn := 0 - if err != nil { - //Unable to load this file - continue - } else { - //Cert loaded. Check its expire time - block, _ := pem.Decode(certBtyes) - if block != nil { - cert, err := x509.ParseCertificate(block.Bytes) - if err == nil { - certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05") - - duration := cert.NotAfter.Sub(time.Now()) - - // Convert the duration to days - expiredIn = int(duration.Hours() / 24) - } - } - } - certInfoFilename := filepath.Join(tlsCertManager.CertStore, filename+".json") - useDNSValidation := false //Default to false for HTTP TLS certificates - certInfo, err := acme.LoadCertInfoJSON(certInfoFilename) //Note: Not all certs have info json - if err == nil { - useDNSValidation = certInfo.UseDNS - } - - thisCertInfo := CertInfo{ - Domain: filename, - LastModifiedDate: modifiedTime, - ExpireDate: certExpireTime, - RemainingDays: expiredIn, - UseDNS: useDNSValidation, - } - - results = append(results, &thisCertInfo) - } - - // convert ExpireDate to date object and sort asc - sort.Slice(results, func(i, j int) bool { - date1, _ := time.Parse("2006-01-02 15:04:05", results[i].ExpireDate) - date2, _ := time.Parse("2006-01-02 15:04:05", results[j].ExpireDate) - return date1.Before(date2) - }) - - js, _ := json.Marshal(results) - w.Header().Set("Content-Type", "application/json") - w.Write(js) - } else { - response, err := json.Marshal(filenames) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(response) - } - -} - -// List all certificates and map all their domains to the cert filename -func handleListDomains(w http.ResponseWriter, r *http.Request) { - filenames, err := os.ReadDir("./conf/certs/") - - if err != nil { - utils.SendErrorResponse(w, err.Error()) - return - } - - certnameToDomainMap := map[string]string{} - for _, filename := range filenames { - if filename.IsDir() { - continue - } - certFilepath := filepath.Join("./conf/certs/", filename.Name()) - - certBtyes, err := os.ReadFile(certFilepath) - if err != nil { - // Unable to load this file - SystemWideLogger.PrintAndLog("TLS", "Unable to load certificate: "+certFilepath, err) - continue - } else { - // Cert loaded. Check its expiry time - block, _ := pem.Decode(certBtyes) - if block != nil { - cert, err := x509.ParseCertificate(block.Bytes) - if err == nil { - certname := strings.TrimSuffix(filepath.Base(certFilepath), filepath.Ext(certFilepath)) - for _, dnsName := range cert.DNSNames { - certnameToDomainMap[dnsName] = certname - } - certnameToDomainMap[cert.Subject.CommonName] = certname - } - } - } - } - - requireCompact, _ := utils.GetPara(r, "compact") - if requireCompact == "true" { - result := make(map[string][]string) - - for key, value := range certnameToDomainMap { - if _, ok := result[value]; !ok { - result[value] = make([]string, 0) - } - - result[value] = append(result[value], key) - } - - js, _ := json.Marshal(result) - utils.SendJSONResponse(w, string(js)) - return - } - - js, _ := json.Marshal(certnameToDomainMap) - utils.SendJSONResponse(w, string(js)) -} - // Handle front-end toggling TLS mode func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) { currentTlsSetting := true //Default to true @@ -193,11 +19,12 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) { sysdb.Read("settings", "usetls", ¤tTlsSetting) } - if r.Method == http.MethodGet { + switch r.Method { + case http.MethodGet: //Get the current status js, _ := json.Marshal(currentTlsSetting) utils.SendJSONResponse(w, string(js)) - } else if r.Method == http.MethodPost { + case http.MethodPost: newState, err := utils.PostBool(r, "set") if err != nil { utils.SendErrorResponse(w, "new state not set or invalid") @@ -213,7 +40,7 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) { dynamicProxyRouter.UpdateTLSSetting(false) } utils.SendOK(w) - } else { + default: http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed) } } @@ -231,135 +58,21 @@ func handleSetTlsRequireLatest(w http.ResponseWriter, r *http.Request) { js, _ := json.Marshal(reqLatestTLS) utils.SendJSONResponse(w, string(js)) } else { - if newState == "true" { + switch newState { + case "true": sysdb.Write("settings", "forceLatestTLS", true) SystemWideLogger.Println("Updating minimum TLS version to v1.2 or above") dynamicProxyRouter.UpdateTLSVersion(true) - } else if newState == "false" { + case "false": sysdb.Write("settings", "forceLatestTLS", false) SystemWideLogger.Println("Updating minimum TLS version to v1.0 or above") dynamicProxyRouter.UpdateTLSVersion(false) - } else { + default: utils.SendErrorResponse(w, "invalid state given") } } } -// Handle download of the selected certificate -func handleCertDownload(w http.ResponseWriter, r *http.Request) { - // get the certificate name - certname, err := utils.GetPara(r, "certname") - if err != nil { - utils.SendErrorResponse(w, "invalid certname given") - return - } - certname = filepath.Base(certname) //prevent path escape - - // check if the cert exists - pubKey := filepath.Join(filepath.Join("./conf/certs"), certname+".key") - priKey := filepath.Join(filepath.Join("./conf/certs"), certname+".pem") - - if utils.FileExists(pubKey) && utils.FileExists(priKey) { - //Zip them and serve them via http download - seeking, _ := utils.GetBool(r, "seek") - if seeking { - //This request only check if the key exists. Do not provide download - utils.SendOK(w) - return - } - - //Serve both file in zip - zipTmpFolder := "./tmp/download" - os.MkdirAll(zipTmpFolder, 0775) - zipFileName := filepath.Join(zipTmpFolder, certname+".zip") - err := utils.ZipFiles(zipFileName, pubKey, priKey) - if err != nil { - http.Error(w, "Failed to create zip file", http.StatusInternalServerError) - return - } - defer os.Remove(zipFileName) // Clean up the zip file after serving - - // Serve the zip file - w.Header().Set("Content-Disposition", "attachment; filename=\""+certname+"_export.zip\"") - w.Header().Set("Content-Type", "application/zip") - http.ServeFile(w, r, zipFileName) - } else { - //Not both key exists - utils.SendErrorResponse(w, "invalid key-pairs: private key or public key not found in key store") - return - } -} - -// Handle upload of the certificate -func handleCertUpload(w http.ResponseWriter, r *http.Request) { - // check if request method is POST - if r.Method != "POST" { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // get the key type - keytype, err := utils.GetPara(r, "ktype") - overWriteFilename := "" - if err != nil { - http.Error(w, "Not defined key type (pub / pri)", http.StatusBadRequest) - return - } - - // get the domain - domain, err := utils.GetPara(r, "domain") - if err != nil { - //Assume localhost - domain = "default" - } - - if keytype == "pub" { - overWriteFilename = domain + ".pem" - } else if keytype == "pri" { - overWriteFilename = domain + ".key" - } else { - http.Error(w, "Not supported keytype: "+keytype, http.StatusBadRequest) - return - } - - // parse multipart form data - err = r.ParseMultipartForm(10 << 20) // 10 MB - if err != nil { - http.Error(w, "Failed to parse form data", http.StatusBadRequest) - return - } - - // get file from form data - file, _, err := r.FormFile("file") - if err != nil { - http.Error(w, "Failed to get file", http.StatusBadRequest) - return - } - defer file.Close() - - // create file in upload directory - os.MkdirAll("./conf/certs", 0775) - f, err := os.Create(filepath.Join("./conf/certs", overWriteFilename)) - if err != nil { - http.Error(w, "Failed to create file", http.StatusInternalServerError) - return - } - defer f.Close() - - // copy file contents to destination file - _, err = io.Copy(f, file) - if err != nil { - http.Error(w, "Failed to save file", http.StatusInternalServerError) - return - } - - //Update cert list - tlsCertManager.UpdateLoadedCertList() - - // send response - fmt.Fprintln(w, "File upload successful!") -} - func handleCertTryResolve(w http.ResponseWriter, r *http.Request) { // get the domain domain, err := utils.GetPara(r, "domain") @@ -441,15 +154,40 @@ func handleCertTryResolve(w http.ResponseWriter, r *http.Request) { utils.SendJSONResponse(w, string(js)) } -// Handle cert remove -func handleCertRemove(w http.ResponseWriter, r *http.Request) { +func handleSetDomainPreferredCertificate(w http.ResponseWriter, r *http.Request) { + //Get the domain domain, err := utils.PostPara(r, "domain") if err != nil { utils.SendErrorResponse(w, "invalid domain given") return } - err = tlsCertManager.RemoveCert(domain) + + //Get the certificate name + certName, err := utils.PostPara(r, "certname") if err != nil { - utils.SendErrorResponse(w, err.Error()) + utils.SendErrorResponse(w, "invalid certificate name given") + return } + + //Load the target endpoint + ept, err := dynamicProxyRouter.GetProxyEndpointById(domain, true) + if err != nil { + utils.SendErrorResponse(w, "proxy rule not found for domain: "+domain) + return + } + + //Set the preferred certificate for the domain + err = dynamicProxyRouter.SetPreferredCertificateForDomain(ept, domain, certName) + if err != nil { + utils.SendErrorResponse(w, "failed to set preferred certificate: "+err.Error()) + return + } + + err = SaveReverseProxyConfig(ept) + if err != nil { + utils.SendErrorResponse(w, "failed to save reverse proxy config: "+err.Error()) + return + } + + utils.SendOK(w) } diff --git a/src/mod/dynamicproxy/Server.go b/src/mod/dynamicproxy/Server.go index 7c195e6..e0b16bb 100644 --- a/src/mod/dynamicproxy/Server.go +++ b/src/mod/dynamicproxy/Server.go @@ -61,7 +61,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { hostPath := strings.Split(r.Host, ":") domainOnly = hostPath[0] } - sep := h.Parent.getProxyEndpointFromHostname(domainOnly) + sep := h.Parent.GetProxyEndpointFromHostname(domainOnly) if sep != nil && !sep.Disabled { //Matching proxy rule found //Access Check (blacklist / whitelist) diff --git a/src/mod/dynamicproxy/certificate.go b/src/mod/dynamicproxy/certificate.go new file mode 100644 index 0000000..9635bea --- /dev/null +++ b/src/mod/dynamicproxy/certificate.go @@ -0,0 +1,59 @@ +package dynamicproxy + +import ( + "encoding/json" + "errors" + "fmt" + + "imuslab.com/zoraxy/mod/tlscert" +) + +func (router *Router) ResolveHostSpecificTlsBehaviorForHostname(hostname string) (*tlscert.HostSpecificTlsBehavior, error) { + if hostname == "" { + return nil, errors.New("hostname cannot be empty") + } + + ept := router.GetProxyEndpointFromHostname(hostname) + if ept == nil { + return tlscert.GetDefaultHostSpecificTlsBehavior(), nil + } + + // Check if the endpoint has a specific TLS behavior + if ept.TlsOptions != nil { + imported := &tlscert.HostSpecificTlsBehavior{} + router.tlsBehaviorMutex.RLock() + // Deep copy the TlsOptions using JSON marshal/unmarshal + data, err := json.Marshal(ept.TlsOptions) + if err != nil { + router.tlsBehaviorMutex.RUnlock() + return nil, fmt.Errorf("failed to deepcopy TlsOptions: %w", err) + } + router.tlsBehaviorMutex.RUnlock() + if err := json.Unmarshal(data, imported); err != nil { + return nil, fmt.Errorf("failed to deepcopy TlsOptions: %w", err) + } + return imported, nil + } + + return tlscert.GetDefaultHostSpecificTlsBehavior(), nil +} + +func (router *Router) SetPreferredCertificateForDomain(ept *ProxyEndpoint, domain string, certName string) error { + if ept == nil || certName == "" { + return errors.New("endpoint and certificate name cannot be empty") + } + + // Set the preferred certificate for the endpoint + if ept.TlsOptions == nil { + ept.TlsOptions = tlscert.GetDefaultHostSpecificTlsBehavior() + } + + router.tlsBehaviorMutex.Lock() + if ept.TlsOptions.PreferredCertificate == nil { + ept.TlsOptions.PreferredCertificate = make(map[string]string) + } + ept.TlsOptions.PreferredCertificate[domain] = certName + router.tlsBehaviorMutex.Unlock() + + return nil +} diff --git a/src/mod/dynamicproxy/dynamicproxy.go b/src/mod/dynamicproxy/dynamicproxy.go index f17181e..8b234f5 100644 --- a/src/mod/dynamicproxy/dynamicproxy.go +++ b/src/mod/dynamicproxy/dynamicproxy.go @@ -111,7 +111,7 @@ func (router *Router) StartProxyService() error { hostPath := strings.Split(r.Host, ":") domainOnly = hostPath[0] } - sep := router.getProxyEndpointFromHostname(domainOnly) + sep := router.GetProxyEndpointFromHostname(domainOnly) if sep != nil && sep.BypassGlobalTLS { //Allow routing via non-TLS handler originalHostHeader := r.Host @@ -335,7 +335,7 @@ func (router *Router) IsProxiedSubdomain(r *http.Request) bool { hostname = r.Host } hostname = strings.Split(hostname, ":")[0] - subdEndpoint := router.getProxyEndpointFromHostname(hostname) + subdEndpoint := router.GetProxyEndpointFromHostname(hostname) return subdEndpoint != nil } diff --git a/src/mod/dynamicproxy/proxyRequestHandler.go b/src/mod/dynamicproxy/proxyRequestHandler.go index 9ba8813..65cb14f 100644 --- a/src/mod/dynamicproxy/proxyRequestHandler.go +++ b/src/mod/dynamicproxy/proxyRequestHandler.go @@ -34,7 +34,7 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P } // Get the proxy endpoint from hostname, which might includes checking of wildcard certificates -func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint { +func (router *Router) GetProxyEndpointFromHostname(hostname string) *ProxyEndpoint { var targetSubdomainEndpoint *ProxyEndpoint = nil hostname = strings.ToLower(hostname) ep, ok := router.ProxyEndpoints.Load(hostname) @@ -63,7 +63,7 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi } //Wildcard not match. Check for alias - if ep.MatchingDomainAlias != nil && len(ep.MatchingDomainAlias) > 0 { + if len(ep.MatchingDomainAlias) > 0 { for _, aliasDomain := range ep.MatchingDomainAlias { match, err := filepath.Match(aliasDomain, hostname) if err != nil { diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index 9f6d63f..f6238a3 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -75,16 +75,20 @@ type RouterOption struct { /* Router Object */ type Router struct { Option *RouterOption - ProxyEndpoints *sync.Map //Map of ProxyEndpoint objects, each ProxyEndpoint object is a routing rule that handle incoming requests - Running bool //If the router is running - Root *ProxyEndpoint //Root proxy endpoint, default site - mux http.Handler //HTTP handler - server *http.Server //HTTP server - tlsListener net.Listener //TLS listener, handle SNI routing - loadBalancer *loadbalance.RouteManager //Load balancer routing manager - routingRules []*RoutingRule //Special routing rules, handle high priority routing like ACME request handling + ProxyEndpoints *sync.Map //Map of ProxyEndpoint objects, each ProxyEndpoint object is a routing rule that handle incoming requests + Running bool //If the router is running + Root *ProxyEndpoint //Root proxy endpoint, default site + + /* Internals */ + mux http.Handler //HTTP handler + server *http.Server //HTTP server + loadBalancer *loadbalance.RouteManager //Load balancer routing manager + routingRules []*RoutingRule //Special routing rules, handle high priority routing like ACME request handling + + tlsListener net.Listener //TLS listener, handle SNI routing + tlsBehaviorMutex sync.RWMutex //Mutex for tlsBehavior map + tlsRedirectStop chan bool //Stop channel for tls redirection server - tlsRedirectStop chan bool //Stop channel for tls redirection server rateLimterStop chan bool //Stop channel for rate limiter rateLimitCounter RequestCountPerIpTable //Request counter for rate limter } diff --git a/src/mod/tlscert/certgen.go b/src/mod/tlscert/certgen.go new file mode 100644 index 0000000..adebad9 --- /dev/null +++ b/src/mod/tlscert/certgen.go @@ -0,0 +1,93 @@ +package tlscert + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "time" +) + +// GenerateSelfSignedCertificate generates a self-signed ECDSA certificate and saves it to the specified files. +func (m *Manager) GenerateSelfSignedCertificate(cn string, sans []string, certFile string, keyFile string) error { + // Generate private key (ECDSA P-256) + privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + m.Logger.PrintAndLog("tls-router", "Failed to generate private key", err) + return err + } + + // Create certificate template + template := x509.Certificate{ + SerialNumber: big.NewInt(time.Now().UnixNano()), + Subject: pkix.Name{ + CommonName: cn, // Common Name for the certificate + Organization: []string{"aroz.org"}, // Organization name + OrganizationalUnit: []string{"Zoraxy"}, // Organizational Unit + Country: []string{"US"}, // Country code + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), // valid for 1 year + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: sans, // Subject Alternative Names + } + + // Create self-signed certificate + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) + if err != nil { + m.Logger.PrintAndLog("tls-router", "Failed to create certificate", err) + return err + } + + // Remove old certificate file if it exists + certPath := filepath.Join(m.CertStore, certFile) + if _, err := os.Stat(certPath); err == nil { + os.Remove(certPath) + } + + // Remove old key file if it exists + keyPath := filepath.Join(m.CertStore, keyFile) + if _, err := os.Stat(keyPath); err == nil { + os.Remove(keyPath) + } + + // Write certificate to file + certOut, err := os.Create(filepath.Join(m.CertStore, certFile)) + if err != nil { + m.Logger.PrintAndLog("tls-router", "Failed to open cert file for writing: "+certFile, err) + return err + } + defer certOut.Close() + err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + if err != nil { + m.Logger.PrintAndLog("tls-router", "Failed to write certificate to file: "+certFile, err) + return err + } + + // Encode private key to PEM + privBytes, err := x509.MarshalECPrivateKey(privKey) + if err != nil { + m.Logger.PrintAndLog("tls-router", "Unable to marshal ECDSA private key", err) + return err + } + keyOut, err := os.Create(filepath.Join(m.CertStore, keyFile)) + if err != nil { + m.Logger.PrintAndLog("tls-router", "Failed to open key file for writing: "+keyFile, err) + return err + } + defer keyOut.Close() + err = pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes}) + if err != nil { + m.Logger.PrintAndLog("tls-router", "Failed to write private key to file: "+keyFile, err) + return err + } + m.Logger.PrintAndLog("tls-router", "Certificate and key generated: "+certFile+", "+keyFile, nil) + return nil +} diff --git a/src/mod/tlscert/handler.go b/src/mod/tlscert/handler.go new file mode 100644 index 0000000..5e9cc7b --- /dev/null +++ b/src/mod/tlscert/handler.go @@ -0,0 +1,352 @@ +package tlscert + +import ( + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "imuslab.com/zoraxy/mod/acme" + "imuslab.com/zoraxy/mod/utils" +) + +// Handle cert remove +func (m *Manager) HandleCertRemove(w http.ResponseWriter, r *http.Request) { + domain, err := utils.PostPara(r, "domain") + if err != nil { + utils.SendErrorResponse(w, "invalid domain given") + return + } + err = m.RemoveCert(domain) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + } +} + +// Handle download of the selected certificate +func (m *Manager) HandleCertDownload(w http.ResponseWriter, r *http.Request) { + // get the certificate name + certname, err := utils.GetPara(r, "certname") + if err != nil { + utils.SendErrorResponse(w, "invalid certname given") + return + } + certname = filepath.Base(certname) //prevent path escape + + // check if the cert exists + pubKey := filepath.Join(filepath.Join(m.CertStore), certname+".key") + priKey := filepath.Join(filepath.Join(m.CertStore), certname+".pem") + + if utils.FileExists(pubKey) && utils.FileExists(priKey) { + //Zip them and serve them via http download + seeking, _ := utils.GetBool(r, "seek") + if seeking { + //This request only check if the key exists. Do not provide download + utils.SendOK(w) + return + } + + //Serve both file in zip + zipTmpFolder := "./tmp/download" + os.MkdirAll(zipTmpFolder, 0775) + zipFileName := filepath.Join(zipTmpFolder, certname+".zip") + err := utils.ZipFiles(zipFileName, pubKey, priKey) + if err != nil { + http.Error(w, "Failed to create zip file", http.StatusInternalServerError) + return + } + defer os.Remove(zipFileName) // Clean up the zip file after serving + + // Serve the zip file + w.Header().Set("Content-Disposition", "attachment; filename=\""+certname+"_export.zip\"") + w.Header().Set("Content-Type", "application/zip") + http.ServeFile(w, r, zipFileName) + } else { + //Not both key exists + utils.SendErrorResponse(w, "invalid key-pairs: private key or public key not found in key store") + return + } +} + +// Handle upload of the certificate +func (m *Manager) HandleCertUpload(w http.ResponseWriter, r *http.Request) { + // check if request method is POST + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // get the key type + keytype, err := utils.GetPara(r, "ktype") + overWriteFilename := "" + if err != nil { + http.Error(w, "Not defined key type (pub / pri)", http.StatusBadRequest) + return + } + + // get the domain + domain, err := utils.GetPara(r, "domain") + if err != nil { + //Assume localhost + domain = "default" + } + + switch keytype { + case "pub": + overWriteFilename = domain + ".pem" + case "pri": + overWriteFilename = domain + ".key" + default: + http.Error(w, "Not supported keytype: "+keytype, http.StatusBadRequest) + return + } + + // parse multipart form data + err = r.ParseMultipartForm(10 << 20) // 10 MB + if err != nil { + http.Error(w, "Failed to parse form data", http.StatusBadRequest) + return + } + + // get file from form data + file, _, err := r.FormFile("file") + if err != nil { + http.Error(w, "Failed to get file", http.StatusBadRequest) + return + } + defer file.Close() + + // create file in upload directory + os.MkdirAll(m.CertStore, 0775) + f, err := os.Create(filepath.Join(m.CertStore, overWriteFilename)) + if err != nil { + http.Error(w, "Failed to create file", http.StatusInternalServerError) + return + } + defer f.Close() + + // copy file contents to destination file + _, err = io.Copy(f, file) + if err != nil { + http.Error(w, "Failed to save file", http.StatusInternalServerError) + return + } + + //Update cert list + m.UpdateLoadedCertList() + + // send response + fmt.Fprintln(w, "File upload successful!") +} + +// List all certificates and map all their domains to the cert filename +func (m *Manager) HandleListDomains(w http.ResponseWriter, r *http.Request) { + filenames, err := os.ReadDir(m.CertStore) + + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + certnameToDomainMap := map[string]string{} + for _, filename := range filenames { + if filename.IsDir() { + continue + } + certFilepath := filepath.Join(m.CertStore, filename.Name()) + + certBtyes, err := os.ReadFile(certFilepath) + if err != nil { + // Unable to load this file + m.Logger.PrintAndLog("TLS", "Unable to load certificate: "+certFilepath, err) + continue + } else { + // Cert loaded. Check its expiry time + block, _ := pem.Decode(certBtyes) + if block != nil { + cert, err := x509.ParseCertificate(block.Bytes) + if err == nil { + certname := strings.TrimSuffix(filepath.Base(certFilepath), filepath.Ext(certFilepath)) + for _, dnsName := range cert.DNSNames { + certnameToDomainMap[dnsName] = certname + } + certnameToDomainMap[cert.Subject.CommonName] = certname + } + } + } + } + + requireCompact, _ := utils.GetPara(r, "compact") + if requireCompact == "true" { + result := make(map[string][]string) + + for key, value := range certnameToDomainMap { + if _, ok := result[value]; !ok { + result[value] = make([]string, 0) + } + + result[value] = append(result[value], key) + } + + js, _ := json.Marshal(result) + utils.SendJSONResponse(w, string(js)) + return + } + + js, _ := json.Marshal(certnameToDomainMap) + utils.SendJSONResponse(w, string(js)) +} + +// Return a list of domains where the certificates covers +func (m *Manager) HandleListCertificate(w http.ResponseWriter, r *http.Request) { + filenames, err := m.ListCertDomains() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + showDate, _ := utils.GetBool(r, "date") + if showDate { + type CertInfo struct { + Domain string + LastModifiedDate string + ExpireDate string + RemainingDays int + UseDNS bool + } + + results := []*CertInfo{} + + for _, filename := range filenames { + certFilepath := filepath.Join(m.CertStore, filename+".pem") + //keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key") + fileInfo, err := os.Stat(certFilepath) + if err != nil { + utils.SendErrorResponse(w, "invalid domain certificate discovered: "+filename) + return + } + modifiedTime := fileInfo.ModTime().Format("2006-01-02 15:04:05") + + certExpireTime := "Unknown" + certBtyes, err := os.ReadFile(certFilepath) + expiredIn := 0 + if err != nil { + //Unable to load this file + continue + } else { + //Cert loaded. Check its expire time + block, _ := pem.Decode(certBtyes) + if block != nil { + cert, err := x509.ParseCertificate(block.Bytes) + if err == nil { + certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05") + + duration := cert.NotAfter.Sub(time.Now()) + + // Convert the duration to days + expiredIn = int(duration.Hours() / 24) + } + } + } + certInfoFilename := filepath.Join(m.CertStore, filename+".json") + useDNSValidation := false //Default to false for HTTP TLS certificates + certInfo, err := acme.LoadCertInfoJSON(certInfoFilename) //Note: Not all certs have info json + if err == nil { + useDNSValidation = certInfo.UseDNS + } + + thisCertInfo := CertInfo{ + Domain: filename, + LastModifiedDate: modifiedTime, + ExpireDate: certExpireTime, + RemainingDays: expiredIn, + UseDNS: useDNSValidation, + } + + results = append(results, &thisCertInfo) + } + + // convert ExpireDate to date object and sort asc + sort.Slice(results, func(i, j int) bool { + date1, _ := time.Parse("2006-01-02 15:04:05", results[i].ExpireDate) + date2, _ := time.Parse("2006-01-02 15:04:05", results[j].ExpireDate) + return date1.Before(date2) + }) + + js, _ := json.Marshal(results) + w.Header().Set("Content-Type", "application/json") + w.Write(js) + return + } + + response, err := json.Marshal(filenames) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(response) +} + +// Check if the default certificates is correctly setup +func (m *Manager) HandleDefaultCertCheck(w http.ResponseWriter, r *http.Request) { + type CheckResult struct { + DefaultPubExists bool + DefaultPriExists bool + } + + pub, pri := m.DefaultCertExistsSep() + js, _ := json.Marshal(CheckResult{ + pub, + pri, + }) + + utils.SendJSONResponse(w, string(js)) +} + +func (m *Manager) HandleSelfSignCertGenerate(w http.ResponseWriter, r *http.Request) { + // Get the common name from the request + cn, err := utils.GetPara(r, "cn") + if err != nil { + utils.SendErrorResponse(w, "Common name not provided") + return + } + + domains, err := utils.PostPara(r, "domains") + if err != nil { + //No alias domains provided, use the common name as the only domain + domains = "[]" + } + + SANs := []string{} + if err := json.Unmarshal([]byte(domains), &SANs); err != nil { + utils.SendErrorResponse(w, "Invalid domains format: "+err.Error()) + return + } + //SANs = append([]string{cn}, SANs...) + priKeyFilename := domainToFilename(cn, ".key") + pubKeyFilename := domainToFilename(cn, ".pem") + + // Generate self-signed certificate + err = m.GenerateSelfSignedCertificate(cn, SANs, pubKeyFilename, priKeyFilename) + if err != nil { + utils.SendErrorResponse(w, "Failed to generate self-signed certificate: "+err.Error()) + return + } + + //Update the certificate store + err = m.UpdateLoadedCertList() + if err != nil { + utils.SendErrorResponse(w, "Failed to update certificate store: "+err.Error()) + return + } + utils.SendOK(w) +} diff --git a/src/mod/tlscert/helper.go b/src/mod/tlscert/helper.go index a637d65..0704723 100644 --- a/src/mod/tlscert/helper.go +++ b/src/mod/tlscert/helper.go @@ -43,3 +43,30 @@ func matchClosestDomainCertificate(subdomain string, domains []string) string { return matchingDomain } + +// Convert a domain name to a filename format +func domainToFilename(domain string, ext string) string { + // Replace wildcard '*' with '_' + domain = strings.TrimSpace(domain) + if strings.HasPrefix(domain, "*") { + domain = "_" + strings.TrimPrefix(domain, "*") + } + + // Add .pem extension + ext = strings.TrimPrefix(ext, ".") // Ensure ext does not start with a dot + return domain + "." + ext +} + +func filenameToDomain(filename string) string { + // Remove the extension + ext := filepath.Ext(filename) + if ext != "" { + filename = strings.TrimSuffix(filename, ext) + } + + if strings.HasPrefix(filename, "_") { + filename = "*" + filename[1:] + } + + return filename +} diff --git a/src/mod/tlscert/tlscert.go b/src/mod/tlscert/tlscert.go index aae4401..a4157c1 100644 --- a/src/mod/tlscert/tlscert.go +++ b/src/mod/tlscert/tlscert.go @@ -21,10 +21,10 @@ type CertCache struct { } type HostSpecificTlsBehavior struct { - DisableSNI bool //If SNI is enabled for this server name - DisableLegacyCertificateMatching bool //If legacy certificate matching is disabled for this server name - EnableAutoHTTPS bool //If auto HTTPS is enabled for this server name - PreferredCertificate string //Preferred certificate for this server name, if empty, use the first matching certificate + DisableSNI bool //If SNI is enabled for this server name + DisableLegacyCertificateMatching bool //If legacy certificate matching is disabled for this server name + EnableAutoHTTPS bool //If auto HTTPS is enabled for this server name + PreferredCertificate map[string]string //Preferred certificate for this server name, if empty, use the first matching certificate } type Manager struct { @@ -34,13 +34,12 @@ type Manager struct { /* External handlers */ hostSpecificTlsBehavior func(serverName string) (*HostSpecificTlsBehavior, error) // Function to get host specific TLS behavior, if nil, use global TLS options - verbal bool } //go:embed localhost.pem localhost.key var buildinCertStore embed.FS -func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager, error) { +func NewManager(certStore string, logger *logger.Logger) (*Manager, error) { if !utils.FileExists(certStore) { os.MkdirAll(certStore, 0775) } @@ -63,7 +62,6 @@ func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager, CertStore: certStore, LoadedCerts: []*CertCache{}, hostSpecificTlsBehavior: defaultHostSpecificTlsBehavior, //Default to no SNI and no auto HTTPS - verbal: verbal, Logger: logger, } @@ -82,7 +80,7 @@ func GetDefaultHostSpecificTlsBehavior() *HostSpecificTlsBehavior { DisableSNI: false, DisableLegacyCertificateMatching: false, EnableAutoHTTPS: false, - PreferredCertificate: "", + PreferredCertificate: map[string]string{}, // No preferred certificate, use the first matching certificate } } @@ -90,6 +88,10 @@ func defaultHostSpecificTlsBehavior(serverName string) (*HostSpecificTlsBehavior return GetDefaultHostSpecificTlsBehavior(), nil } +func (m *Manager) SetHostSpecificTlsBehavior(fn func(serverName string) (*HostSpecificTlsBehavior, error)) { + m.hostSpecificTlsBehavior = fn +} + // Update domain mapping from file func (m *Manager) UpdateLoadedCertList() error { //Get a list of certificates from file @@ -213,13 +215,17 @@ func (m *Manager) GetCertificateByHostname(hostname string) (string, string, err if err != nil { tlsBehavior, _ = defaultHostSpecificTlsBehavior(hostname) } + preferredCertificate, ok := tlsBehavior.PreferredCertificate[hostname] + if !ok { + preferredCertificate = "" + } - if tlsBehavior.DisableSNI && tlsBehavior.PreferredCertificate != "" && - utils.FileExists(filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".pem")) && - utils.FileExists(filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".key")) { + if tlsBehavior.DisableSNI && preferredCertificate != "" && + utils.FileExists(filepath.Join(m.CertStore, preferredCertificate+".pem")) && + utils.FileExists(filepath.Join(m.CertStore, preferredCertificate+".key")) { //User setup a Preferred certificate, use the preferred certificate directly - pubKey = filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".pem") - priKey = filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".key") + pubKey = filepath.Join(m.CertStore, preferredCertificate+".pem") + priKey = filepath.Join(m.CertStore, preferredCertificate+".key") } else { if !tlsBehavior.DisableLegacyCertificateMatching && utils.FileExists(filepath.Join(m.CertStore, hostname+".pem")) && diff --git a/src/reverseproxy.go b/src/reverseproxy.go index 0054f1e..6bc5e07 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -140,7 +140,7 @@ func ReverseProxtInit() { err := LoadReverseProxyConfig(conf) if err != nil { SystemWideLogger.PrintAndLog("proxy-config", "Failed to load config file: "+filepath.Base(conf), err) - return + continue } } @@ -717,6 +717,11 @@ func ReverseProxyHandleSetTlsConfig(w http.ResponseWriter, r *http.Request) { return } + if newTlsConfig.PreferredCertificate == nil { + //No update needed, reuse the current TLS config + newTlsConfig.PreferredCertificate = ept.TlsOptions.PreferredCertificate + } + ept.TlsOptions = newTlsConfig //Prepare to replace the current routing rule diff --git a/src/start.go b/src/start.go index 7f191ea..359da3a 100644 --- a/src/start.go +++ b/src/start.go @@ -1,7 +1,6 @@ package main import ( - "imuslab.com/zoraxy/mod/auth/sso/oauth2" "log" "net/http" "os" @@ -10,6 +9,8 @@ import ( "strings" "time" + "imuslab.com/zoraxy/mod/auth/sso/oauth2" + "github.com/gorilla/csrf" "imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/acme" @@ -99,7 +100,7 @@ func startupSequence() { }) //Create a TLS certificate manager - tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, *development_build, SystemWideLogger) + tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, SystemWideLogger) if err != nil { panic(err) } @@ -366,6 +367,9 @@ func finalSequence() { //Inject routing rules registerBuildInRoutingRules() + + //Set the host specific TLS behavior resolver for resolving TLS behavior for each hostname + tlsCertManager.SetHostSpecificTlsBehavior(dynamicProxyRouter.ResolveHostSpecificTlsBehaviorForHostname) } /* Shutdown Sequence */ diff --git a/src/web/components/httprp.html b/src/web/components/httprp.html index 1f29a5f..1293b07 100644 --- a/src/web/components/httprp.html +++ b/src/web/components/httprp.html @@ -339,11 +339,15 @@

The table below shows which certificate will be served by Zoraxy when a client request the following hostnames.

- + +
- + @@ -359,18 +363,20 @@
-
+
-

+
+
@@ -747,7 +753,7 @@ let newTlsOption = { "DisableSNI": !enableSNI, "DisableLegacyCertificateMatching": !enableLegacyCertificateMatching, - "EnableAutoHTTPS": enableAutoHTTPS + "EnableAutoHTTPS": enableAutoHTTPS, } $.cjax({ url: "/api/proxy/setTlsConfig", @@ -769,6 +775,9 @@ function updateTlsResolveList(uuid){ let editor = $("#httprpEditModalWrapper"); + editor.find(".certificateDropdown .ui.dropdown").off("change"); + editor.find(".certificateDropdown .ui.dropdown").remove(); + //Update the TLS resolve list $.ajax({ url: "/api/cert/resolve?domain=" + uuid, @@ -785,17 +794,60 @@ resolveList.append(` - + `); aliasDomains.forEach(alias => { resolveList.append(` - + `); }); + + //Generate the certificate dropdown + generateCertificateDropdown(function(dropdown) { + let SNIEnabled = editor.find(".Tls_EnableSNI")[0].checked; + editor.find(".certificateDropdown").html(dropdown); + editor.find(".certificateDropdown").each(function() { + let dropdownDomain = $(this).attr("domain"); + let selectedCertname = certMap[dropdownDomain]; + if (selectedCertname) { + $(this).find(".ui.dropdown").dropdown("set selected", selectedCertname); + } + }); + + editor.find(".certificateDropdown .ui.dropdown").dropdown({ + onChange: function(value, text, $selectedItem) { + console.log("Selected certificate for domain:", $(this).parent().attr("domain"), "Value:", value); + let domain = $(this).parent().attr("domain"); + let newCertificateName = value; + $.cjax({ + url: "/api/cert/setPreferredCertificate", + method: "POST", + data: { + "domain": domain, + "certname": newCertificateName + }, + success: function(data) { + if (data.error !== undefined) { + msgbox(data.error, false, 3000); + } else { + msgbox("Preferred Certificate updated"); + } + } + }); + } + }); + + if (SNIEnabled) { + editor.find(".certificateDropdown .ui.dropdown").addClass("disabled"); + editor.find(".sni_grey_out_info").show(); + }else{ + editor.find(".sni_grey_out_info").hide(); + } + }); } }); } @@ -946,6 +998,29 @@ renewCertificate(renewDomainKey, false, btn); } + function generateSelfSignedCertificate(uuid, domains, btn=undefined){ + let payload = JSON.stringify(domains); + $.cjax({ + url: "/api/cert/selfsign", + data: { + "cn": uuid, + "domains": payload + }, + success: function(data){ + if (data.error == undefined){ + msgbox("Self-Signed Certificate Generated", true); + resyncProxyEditorConfig(); + if (typeof(initManagedDomainCertificateList) != undefined){ + //Re-init the managed domain certificate list + initManagedDomainCertificateList(); + } + }else{ + msgbox(data.error, false); + } + } + }); + } + /* Tags & Search */ function handleSearchInput(event){ if (event.key == "Escape"){ @@ -1074,6 +1149,28 @@ return subd; } + // Generate a certificate dropdown for the HTTP Proxy Rule Editor + // so user can pick which certificate they want to use for the current editing hostname + function generateCertificateDropdown(callback){ + $.ajax({ + url: "/api/cert/list", + method: "GET", + success: function(data) { + let dropdown = $(''); + let menu = $(''); + data.forEach(cert => { + menu.append(`
${cert}
`); + }); + // Add a hidden input to store the selected certificate + dropdown.append(''); + dropdown.append(''); + dropdown.append('
Fallback Certificate
'); + dropdown.append(menu); + callback(dropdown); + } + }) + } + //Initialize the http proxy rule editor function initHttpProxyRuleEditorModal(rulepayload){ let subd = JSON.parse(JSON.stringify(rulepayload)); @@ -1175,39 +1272,6 @@ }); editor.find(".downstream_alias_hostname").html(aliasHTML); - - //TODO: Move this to SSL TLS section - let enableQuickRequestButton = true; - let domains = [subd.RootOrMatchingDomain]; //Domain for getting certificate if needed - for (var i = 0; i < subd.MatchingDomainAlias.length; i++){ - let thisAliasName = subd.MatchingDomainAlias[i]; - domains.push(thisAliasName); - } - - //Check if the domain or alias contains wildcard, if yes, disabled the get certificate button - if (subd.RootOrMatchingDomain.indexOf("*") > -1){ - enableQuickRequestButton = false; - } - - if (subd.MatchingDomainAlias != undefined){ - for (var i = 0; i < subd.MatchingDomainAlias.length; i++){ - if (subd.MatchingDomainAlias[i].indexOf("*") > -1){ - enableQuickRequestButton = false; - break; - } - } - } - - let certificateDomains = encodeURIComponent(JSON.stringify(domains)); - if (enableQuickRequestButton){ - editor.find(".getCertificateBtn").removeClass("disabled"); - }else{ - editor.find(".getCertificateBtn").addClass("disabled"); - } - - editor.find(".getCertificateBtn").off("click").on("click", function(){ - requestCertificateForExistingHost(uuid, certificateDomains, this); - }); /* ------------ Upstreams ------------ */ editor.find(".upstream_list").html(renderUpstreamList(subd)); @@ -1237,6 +1301,8 @@ editor.find(".vdir_list").html(renderVirtualDirectoryList(subd)); editor.find(".editVdirBtn").off("click").on("click", function(){ quickEditVdir(uuid); + //Temporary restore scroll + $("body").css("overflow", "auto"); }); /* ------------ Alias ------------ */ @@ -1336,6 +1402,7 @@ /* ------------ TLS ------------ */ updateTlsResolveList(uuid); editor.find(".Tls_EnableSNI").prop("checked", !subd.TlsOptions.DisableSNI); + editor.find(".Tls_EnableLegacyCertificateMatching").prop("checked", !subd.TlsOptions.DisableLegacyCertificateMatching); editor.find(".Tls_EnableAutoHTTPS").prop("checked", !!subd.TlsOptions.EnableAutoHTTPS); @@ -1349,6 +1416,45 @@ saveTlsConfigs(uuid); }); + /* Quick access to get certificate for the current host */ + let enableQuickRequestButton = true; + let domains = [subd.RootOrMatchingDomain]; //Domain for getting certificate if needed + for (var i = 0; i < subd.MatchingDomainAlias.length; i++){ + let thisAliasName = subd.MatchingDomainAlias[i]; + domains.push(thisAliasName); + } + + //Check if the domain or alias contains wildcard, if yes, disabled the get certificate button + if (subd.RootOrMatchingDomain.indexOf("*") > -1){ + enableQuickRequestButton = false; + } + + if (subd.MatchingDomainAlias != undefined){ + for (var i = 0; i < subd.MatchingDomainAlias.length; i++){ + if (subd.MatchingDomainAlias[i].indexOf("*") > -1){ + enableQuickRequestButton = false; + break; + } + } + } + if (enableQuickRequestButton){ + editor.find(".getCertificateBtn").removeClass("disabled"); + }else{ + editor.find(".getCertificateBtn").addClass("disabled"); + } + + editor.find(".getCertificateBtn").off("click").on("click", function(){ + let certificateDomains = encodeURIComponent(JSON.stringify(domains)); + requestCertificateForExistingHost(uuid, certificateDomains, this); + }); + + // Bind event to self-signed certificate button + editor.find(".getSelfSignCertBtn").off("click").on("click", function() { + generateSelfSignedCertificate(uuid, domains, this); + }); + + + /* ------------ Tags ------------ */ (()=>{ let payload = encodeURIComponent(JSON.stringify({ @@ -1411,7 +1517,6 @@ }); } - /* Page Initialization Functions */ @@ -1436,7 +1541,9 @@ // there is a chance where the user has modified the Vdir // we need to get the latest setting from server side and // render it again - updateVdirInProxyEditor(); + resyncProxyEditorConfig(); + window.scrollTo(0, 0); + $("body").css("overflow", "hidden"); } else { listProxyEndpoints(); //Reset the tag filter diff --git a/src/web/components/sso.html b/src/web/components/sso.html index e8531c7..0224f04 100644 --- a/src/web/components/sso.html +++ b/src/web/components/sso.html @@ -151,11 +151,31 @@ dataType: 'json', success: function(data) { $('#forwardAuthAddress').val(data.address); - $('#forwardAuthResponseHeaders').val(data.responseHeaders.join(",")); - $('#forwardAuthResponseClientHeaders').val(data.responseClientHeaders.join(",")); - $('#forwardAuthRequestHeaders').val(data.requestHeaders.join(",")); - $('#forwardAuthRequestIncludedCookies').val(data.requestIncludedCookies.join(",")); - $('#forwardAuthRequestExcludedCookies').val(data.requestExcludedCookies.join(",")); + if (data.responseHeaders != null) { + $('#forwardAuthResponseHeaders').val(data.responseHeaders.join(",")); + } else { + $('#forwardAuthResponseHeaders').val(""); + } + if (data.responseClientHeaders != null) { + $('#forwardAuthResponseClientHeaders').val(data.responseClientHeaders.join(",")); + } else { + $('#forwardAuthResponseClientHeaders').val(""); + } + if (data.requestHeaders != null) { + $('#forwardAuthRequestHeaders').val(data.requestHeaders.join(",")); + } else { + $('#forwardAuthRequestHeaders').val(""); + } + if (data.requestIncludedCookies != null) { + $('#forwardAuthRequestIncludedCookies').val(data.requestIncludedCookies.join(",")); + } else { + $('#forwardAuthRequestIncludedCookies').val(""); + } + if (data.requestExcludedCookies != null) { + $('#forwardAuthRequestExcludedCookies').val(data.requestExcludedCookies.join(",")); + } else { + $('#forwardAuthRequestExcludedCookies').val(""); + } }, error: function(jqXHR, textStatus, errorThrown) { console.error('Error fetching SSO settings:', textStatus, errorThrown);
HostnameResolve to CertificateResolve to Certificate
${primaryDomain}${certMap[primaryDomain] || "Fallback Certificate"}${certMap[primaryDomain] || "Fallback Certificate"}
${alias}${certMap[alias] || "Fallback Certificate"}${certMap[alias] || "Fallback Certificate"}