mirror of
				https://github.com/tobychui/zoraxy.git
				synced 2025-10-31 05:54:04 +01:00 
			
		
		
		
	Merge pull request #751 from tobychui/v3.2.5
* Added new API endpoint /api/proxy/setTlsConfig (for HTTP Proxy Editor TLS tab) * Refactored TLS certificate management APIs with new handlers * Removed redundant functions from src/cert.go and delegated to tlsCertManager * Code optimization in tlscert module * Introduced a new constant CONF_FOLDER and updated configuration storage paths (phasing out hard coded paths) * Updated functions to set default TLS options when missing, default to SNI By @jemmy1794 * Added Proxy Protocol v1 support in stream proxy * Fixed Proxy UI bug
This commit is contained in:
		| @@ -200,6 +200,10 @@ Some section of Zoraxy are contributed by our amazing community and if you have | ||||
|  | ||||
| - Docker Container List by [@eyerrock](https://github.com/eyerrock) | ||||
|  | ||||
| - Stream Proxy [@jemmy1794](https://github.com/jemmy1794) | ||||
|  | ||||
| - Change Log [@Morethanevil](https://github.com/Morethanevil) | ||||
|  | ||||
| ### Looking for Maintainer | ||||
|  | ||||
| - ACME DNS Challenge Module | ||||
|   | ||||
							
								
								
									
										19
									
								
								src/api.go
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								src/api.go
									
									
									
									
									
								
							| @@ -34,6 +34,7 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) { | ||||
| 	authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail) | ||||
| 	authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint) | ||||
| 	authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias) | ||||
| 	authRouter.HandleFunc("/api/proxy/setTlsConfig", ReverseProxyHandleSetTlsConfig) | ||||
| 	authRouter.HandleFunc("/api/proxy/setHostname", ReverseProxyHandleSetHostname) | ||||
| 	authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint) | ||||
| 	authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials) | ||||
| @@ -71,14 +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 | ||||
|   | ||||
							
								
								
									
										381
									
								
								src/cert.go
									
									
									
									
									
								
							
							
						
						
									
										381
									
								
								src/cert.go
									
									
									
									
									
								
							| @@ -1,180 +1,14 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"crypto/x509" | ||||
| 	"encoding/json" | ||||
| 	"encoding/pem" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"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) | ||||
| 		} | ||||
|  | ||||
| 		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 | ||||
| @@ -185,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") | ||||
| @@ -205,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) | ||||
| 	} | ||||
| } | ||||
| @@ -223,144 +58,136 @@ 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 | ||||
| 	} | ||||
|  | ||||
| func handleCertTryResolve(w http.ResponseWriter, r *http.Request) { | ||||
| 	// 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) | ||||
| 		utils.SendErrorResponse(w, "invalid domain given") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// parse multipart form data | ||||
| 	err = r.ParseMultipartForm(10 << 20) // 10 MB | ||||
| 	// get the proxy rule, the pass in domain value must be root or matching domain | ||||
| 	proxyRule, err := dynamicProxyRouter.GetProxyEndpointById(domain, false) | ||||
| 	if err != nil { | ||||
| 		http.Error(w, "Failed to parse form data", http.StatusBadRequest) | ||||
| 		return | ||||
| 		//Try to resolve the domain via alias | ||||
| 		proxyRule, err = dynamicProxyRouter.GetProxyEndpointByAlias(domain) | ||||
| 		if err != nil { | ||||
| 			//No matching rule found | ||||
| 			utils.SendErrorResponse(w, "proxy rule not found for domain: "+domain) | ||||
| 			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 | ||||
| 	// list all the alias domains for this rule | ||||
| 	allDomains := []string{proxyRule.RootOrMatchingDomain} | ||||
| 	aliasDomains := []string{} | ||||
| 	for _, alias := range proxyRule.MatchingDomainAlias { | ||||
| 		if alias != "" { | ||||
| 			aliasDomains = append(aliasDomains, alias) | ||||
| 			allDomains = append(allDomains, alias) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	//Update cert list | ||||
| 	tlsCertManager.UpdateLoadedCertList() | ||||
| 	// Try to resolve the domain | ||||
| 	domainKeyPairs := map[string]string{} | ||||
| 	for _, thisDomain := range allDomains { | ||||
| 		pubkey, prikey, err := tlsCertManager.GetCertificateByHostname(thisDomain) | ||||
| 		if err != nil { | ||||
| 			utils.SendErrorResponse(w, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 	// send response | ||||
| 	fmt.Fprintln(w, "File upload successful!") | ||||
| 		//Make sure pubkey and private key are not empty | ||||
| 		if pubkey == "" || prikey == "" { | ||||
| 			domainKeyPairs[thisDomain] = "" | ||||
| 		} else { | ||||
| 			//Store the key pair | ||||
| 			keyname := strings.TrimSuffix(filepath.Base(pubkey), filepath.Ext(pubkey)) | ||||
| 			if keyname == "localhost" { | ||||
| 				//Internal certs like localhost should not be used | ||||
| 				//report as "fallback" key | ||||
| 				keyname = "fallback certificate" | ||||
| 			} | ||||
| 			domainKeyPairs[thisDomain] = keyname | ||||
| 		} | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	//A domain must be UseDNSValidation if it is a wildcard domain or its alias is a wildcard domain | ||||
| 	useDNSValidation := strings.HasPrefix(proxyRule.RootOrMatchingDomain, "*") | ||||
| 	for _, alias := range aliasDomains { | ||||
| 		if strings.HasPrefix(alias, "*") || strings.HasPrefix(domain, "*") { | ||||
| 			useDNSValidation = true | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	type CertInfo struct { | ||||
| 		Domain           string            `json:"domain"` | ||||
| 		AliasDomains     []string          `json:"alias_domains"` | ||||
| 		DomainKeyPair    map[string]string `json:"domain_key_pair"` | ||||
| 		UseDNSValidation bool              `json:"use_dns_validation"` | ||||
| 	} | ||||
|  | ||||
| 	result := &CertInfo{ | ||||
| 		Domain:           proxyRule.RootOrMatchingDomain, | ||||
| 		AliasDomains:     aliasDomains, | ||||
| 		DomainKeyPair:    domainKeyPairs, | ||||
| 		UseDNSValidation: useDNSValidation, | ||||
| 	} | ||||
|  | ||||
| 	js, _ := json.Marshal(result) | ||||
| 	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) | ||||
| } | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import ( | ||||
|  | ||||
| 	"imuslab.com/zoraxy/mod/dynamicproxy" | ||||
| 	"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" | ||||
| 	"imuslab.com/zoraxy/mod/tlscert" | ||||
| 	"imuslab.com/zoraxy/mod/utils" | ||||
| ) | ||||
|  | ||||
| @@ -59,12 +60,18 @@ func LoadReverseProxyConfig(configFilepath string) error { | ||||
| 		thisConfigEndpoint.Tags = []string{} | ||||
| 	} | ||||
|  | ||||
| 	//Make sure the TLS options are not nil | ||||
| 	if thisConfigEndpoint.TlsOptions == nil { | ||||
| 		thisConfigEndpoint.TlsOptions = tlscert.GetDefaultHostSpecificTlsBehavior() | ||||
| 	} | ||||
|  | ||||
| 	//Matching domain not set. Assume root | ||||
| 	if thisConfigEndpoint.RootOrMatchingDomain == "" { | ||||
| 		thisConfigEndpoint.RootOrMatchingDomain = "/" | ||||
| 	} | ||||
|  | ||||
| 	if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeRoot { | ||||
| 	switch thisConfigEndpoint.ProxyType { | ||||
| 	case dynamicproxy.ProxyTypeRoot: | ||||
| 		//This is a root config file | ||||
| 		rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint) | ||||
| 		if err != nil { | ||||
| @@ -73,7 +80,7 @@ func LoadReverseProxyConfig(configFilepath string) error { | ||||
|  | ||||
| 		dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint) | ||||
|  | ||||
| 	} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeHost { | ||||
| 	case dynamicproxy.ProxyTypeHost: | ||||
| 		//This is a host config file | ||||
| 		readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint) | ||||
| 		if err != nil { | ||||
| @@ -81,7 +88,7 @@ func LoadReverseProxyConfig(configFilepath string) error { | ||||
| 		} | ||||
|  | ||||
| 		dynamicProxyRouter.AddProxyRouteToRuntime(readyProxyEndpoint) | ||||
| 	} else { | ||||
| 	default: | ||||
| 		return errors.New("not supported proxy type") | ||||
| 	} | ||||
|  | ||||
| @@ -101,9 +108,9 @@ func filterProxyConfigFilename(filename string) string { | ||||
|  | ||||
| func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error { | ||||
| 	//Get filename for saving | ||||
| 	filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config") | ||||
| 	filename := filepath.Join(CONF_HTTP_PROXY, endpoint.RootOrMatchingDomain+".config") | ||||
| 	if endpoint.ProxyType == dynamicproxy.ProxyTypeRoot { | ||||
| 		filename = "./conf/proxy/root.config" | ||||
| 		filename = filepath.Join(CONF_HTTP_PROXY, "root.config") | ||||
| 	} | ||||
|  | ||||
| 	filename = filterProxyConfigFilename(filename) | ||||
| @@ -118,9 +125,9 @@ func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error { | ||||
| } | ||||
|  | ||||
| func RemoveReverseProxyConfig(endpoint string) error { | ||||
| 	filename := filepath.Join("./conf/proxy/", endpoint+".config") | ||||
| 	filename := filepath.Join(CONF_HTTP_PROXY, endpoint+".config") | ||||
| 	if endpoint == "/" { | ||||
| 		filename = "./conf/proxy/root.config" | ||||
| 		filename = filepath.Join(CONF_HTTP_PROXY, "/root.config") | ||||
| 	} | ||||
|  | ||||
| 	filename = filterProxyConfigFilename(filename) | ||||
| @@ -172,11 +179,11 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) { | ||||
| 	} | ||||
|  | ||||
| 	// Specify the folder path to be zipped | ||||
| 	if !utils.FileExists("./conf") { | ||||
| 	if !utils.FileExists(CONF_FOLDER) { | ||||
| 		SystemWideLogger.PrintAndLog("Backup", "Configuration folder not found", nil) | ||||
| 		return | ||||
| 	} | ||||
| 	folderPath := "./conf" | ||||
| 	folderPath := CONF_FOLDER | ||||
|  | ||||
| 	// Set the Content-Type header to indicate it's a zip file | ||||
| 	w.Header().Set("Content-Type", "application/zip") | ||||
| @@ -277,12 +284,12 @@ func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) { | ||||
| 		return | ||||
| 	} | ||||
| 	// Create the target directory to unzip the files | ||||
| 	targetDir := "./conf" | ||||
| 	targetDir := CONF_FOLDER | ||||
| 	if utils.FileExists(targetDir) { | ||||
| 		//Backup the old config to old | ||||
| 		//backupPath := filepath.Dir(*path_conf) + filepath.Base(*path_conf) + ".old_" + strconv.Itoa(int(time.Now().Unix())) | ||||
| 		//os.Rename(*path_conf, backupPath) | ||||
| 		os.Rename("./conf", "./conf.old_"+strconv.Itoa(int(time.Now().Unix()))) | ||||
| 		os.Rename(CONF_FOLDER, CONF_FOLDER+".old_"+strconv.Itoa(int(time.Now().Unix()))) | ||||
| 	} | ||||
|  | ||||
| 	err = os.MkdirAll(targetDir, os.ModePerm) | ||||
|   | ||||
							
								
								
									
										23
									
								
								src/def.go
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								src/def.go
									
									
									
									
									
								
							| @@ -44,7 +44,7 @@ import ( | ||||
| const ( | ||||
| 	/* Build Constants */ | ||||
| 	SYSTEM_NAME       = "Zoraxy" | ||||
| 	SYSTEM_VERSION    = "3.2.4" | ||||
| 	SYSTEM_VERSION    = "3.2.5" | ||||
| 	DEVELOPMENT_BUILD = false | ||||
|  | ||||
| 	/* System Constants */ | ||||
| @@ -63,14 +63,19 @@ const ( | ||||
| 	LOG_EXTENSION                = ".log" | ||||
| 	STATISTIC_AUTO_SAVE_INTERVAL = 600 /* Seconds */ | ||||
|  | ||||
| 	/* Configuration Folder Storage Path Constants */ | ||||
| 	CONF_HTTP_PROXY    = "./conf/proxy" | ||||
| 	CONF_STREAM_PROXY  = "./conf/streamproxy" | ||||
| 	CONF_CERT_STORE    = "./conf/certs" | ||||
| 	CONF_REDIRECTION   = "./conf/redirect" | ||||
| 	CONF_ACCESS_RULE   = "./conf/access" | ||||
| 	CONF_PATH_RULE     = "./conf/rules/pathrules" | ||||
| 	CONF_PLUGIN_GROUPS = "./conf/plugin_groups.json" | ||||
| 	/* | ||||
| 		Configuration Folder Storage Path Constants | ||||
| 		Note: No tailing slash in the path | ||||
| 	*/ | ||||
| 	CONF_FOLDER        = "./conf" | ||||
| 	CONF_HTTP_PROXY    = CONF_FOLDER + "/proxy" | ||||
| 	CONF_STREAM_PROXY  = CONF_FOLDER + "/streamproxy" | ||||
| 	CONF_CERT_STORE    = CONF_FOLDER + "/certs" | ||||
| 	CONF_REDIRECTION   = CONF_FOLDER + "/redirect" | ||||
| 	CONF_ACCESS_RULE   = CONF_FOLDER + "/access" | ||||
| 	CONF_PATH_RULE     = CONF_FOLDER + "/rules/pathrules" | ||||
| 	CONF_PLUGIN_GROUPS = CONF_FOLDER + "/plugin_groups.json" | ||||
| 	CONF_GEODB_PATH    = CONF_FOLDER + "/geodb" | ||||
| ) | ||||
|  | ||||
| /* System Startup Flags */ | ||||
|   | ||||
| @@ -69,7 +69,7 @@ func main() { | ||||
| 		os.Exit(0) | ||||
| 	} | ||||
| 	if *geoDbUpdate { | ||||
| 		geodb.DownloadGeoDBUpdate("./conf/geodb") | ||||
| 		geodb.DownloadGeoDBUpdate(CONF_GEODB_PATH) | ||||
| 		os.Exit(0) | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -58,11 +58,20 @@ func NewAuthRouter(options *AuthRouterOptions) *AuthRouter { | ||||
| 	options.Database.Read(DatabaseTable, DatabaseKeyRequestIncludedCookies, &requestIncludedCookies) | ||||
| 	options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, &requestExcludedCookies) | ||||
|  | ||||
| 	options.ResponseHeaders = strings.Split(responseHeaders, ",") | ||||
| 	options.ResponseClientHeaders = strings.Split(responseClientHeaders, ",") | ||||
| 	options.RequestHeaders = strings.Split(requestHeaders, ",") | ||||
| 	options.RequestIncludedCookies = strings.Split(requestIncludedCookies, ",") | ||||
| 	options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",") | ||||
| 	// Helper function to clean empty strings from split results | ||||
| 	cleanSplit := func(s string) []string { | ||||
| 	        if s == "" { | ||||
| 	          return nil | ||||
| 	        } | ||||
|  | ||||
| 		return strings.Split(s, ",") | ||||
| 	} | ||||
|  | ||||
| 	options.ResponseHeaders = cleanSplit(responseHeaders) | ||||
| 	options.ResponseClientHeaders = cleanSplit(responseClientHeaders) | ||||
| 	options.RequestHeaders = cleanSplit(requestHeaders) | ||||
| 	options.RequestIncludedCookies = cleanSplit(requestIncludedCookies) | ||||
| 	options.RequestExcludedCookies = cleanSplit(requestExcludedCookies) | ||||
|  | ||||
| 	return &AuthRouter{ | ||||
| 		client: &http.Client{ | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
							
								
								
									
										59
									
								
								src/mod/dynamicproxy/certificate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/mod/dynamicproxy/certificate.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										123
									
								
								src/mod/dynamicproxy/permissionpolicy/contentsecuritypolicy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/mod/dynamicproxy/permissionpolicy/contentsecuritypolicy.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| package permissionpolicy | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| /* | ||||
| 	Content Security Policy | ||||
|  | ||||
| 	This is a content security policy header modifier that changes | ||||
| 	the request content security policy fields | ||||
|  | ||||
| 	author: tobychui | ||||
|  | ||||
| 	//TODO: intergrate this with the dynamic proxy module | ||||
| */ | ||||
|  | ||||
| type ContentSecurityPolicy struct { | ||||
| 	DefaultSrc              []string `json:"default_src"` | ||||
| 	ScriptSrc               []string `json:"script_src"` | ||||
| 	StyleSrc                []string `json:"style_src"` | ||||
| 	ImgSrc                  []string `json:"img_src"` | ||||
| 	ConnectSrc              []string `json:"connect_src"` | ||||
| 	FontSrc                 []string `json:"font_src"` | ||||
| 	ObjectSrc               []string `json:"object_src"` | ||||
| 	MediaSrc                []string `json:"media_src"` | ||||
| 	FrameSrc                []string `json:"frame_src"` | ||||
| 	WorkerSrc               []string `json:"worker_src"` | ||||
| 	ChildSrc                []string `json:"child_src"` | ||||
| 	ManifestSrc             []string `json:"manifest_src"` | ||||
| 	PrefetchSrc             []string `json:"prefetch_src"` | ||||
| 	FormAction              []string `json:"form_action"` | ||||
| 	FrameAncestors          []string `json:"frame_ancestors"` | ||||
| 	BaseURI                 []string `json:"base_uri"` | ||||
| 	Sandbox                 []string `json:"sandbox"` | ||||
| 	ReportURI               []string `json:"report_uri"` | ||||
| 	ReportTo                []string `json:"report_to"` | ||||
| 	UpgradeInsecureRequests bool     `json:"upgrade_insecure_requests"` | ||||
| 	BlockAllMixedContent    bool     `json:"block_all_mixed_content"` | ||||
| } | ||||
|  | ||||
| // GetDefaultContentSecurityPolicy returns a ContentSecurityPolicy struct with default permissive settings | ||||
| func GetDefaultContentSecurityPolicy() *ContentSecurityPolicy { | ||||
| 	return &ContentSecurityPolicy{ | ||||
| 		DefaultSrc:              []string{"*"}, | ||||
| 		ScriptSrc:               []string{"*"}, | ||||
| 		StyleSrc:                []string{"*"}, | ||||
| 		ImgSrc:                  []string{"*"}, | ||||
| 		ConnectSrc:              []string{"*"}, | ||||
| 		FontSrc:                 []string{"*"}, | ||||
| 		ObjectSrc:               []string{"*"}, | ||||
| 		MediaSrc:                []string{"*"}, | ||||
| 		FrameSrc:                []string{"*"}, | ||||
| 		WorkerSrc:               []string{"*"}, | ||||
| 		ChildSrc:                []string{"*"}, | ||||
| 		ManifestSrc:             []string{"*"}, | ||||
| 		PrefetchSrc:             []string{"*"}, | ||||
| 		FormAction:              []string{"*"}, | ||||
| 		FrameAncestors:          []string{"*"}, | ||||
| 		BaseURI:                 []string{"*"}, | ||||
| 		Sandbox:                 []string{}, | ||||
| 		ReportURI:               []string{}, | ||||
| 		ReportTo:                []string{}, | ||||
| 		UpgradeInsecureRequests: false, | ||||
| 		BlockAllMixedContent:    false, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // ToHeader converts a ContentSecurityPolicy struct into a CSP header key-value pair | ||||
| func (csp *ContentSecurityPolicy) ToHeader() []string { | ||||
| 	directives := []string{} | ||||
|  | ||||
| 	addDirective := func(name string, sources []string) { | ||||
| 		if len(sources) > 0 { | ||||
| 			directives = append(directives, name+" "+strings.Join(sources, " ")) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	addDirective("default-src", csp.DefaultSrc) | ||||
| 	addDirective("script-src", csp.ScriptSrc) | ||||
| 	addDirective("style-src", csp.StyleSrc) | ||||
| 	addDirective("img-src", csp.ImgSrc) | ||||
| 	addDirective("connect-src", csp.ConnectSrc) | ||||
| 	addDirective("font-src", csp.FontSrc) | ||||
| 	addDirective("object-src", csp.ObjectSrc) | ||||
| 	addDirective("media-src", csp.MediaSrc) | ||||
| 	addDirective("frame-src", csp.FrameSrc) | ||||
| 	addDirective("worker-src", csp.WorkerSrc) | ||||
| 	addDirective("child-src", csp.ChildSrc) | ||||
| 	addDirective("manifest-src", csp.ManifestSrc) | ||||
| 	addDirective("prefetch-src", csp.PrefetchSrc) | ||||
| 	addDirective("form-action", csp.FormAction) | ||||
| 	addDirective("frame-ancestors", csp.FrameAncestors) | ||||
| 	addDirective("base-uri", csp.BaseURI) | ||||
| 	if len(csp.Sandbox) > 0 { | ||||
| 		directives = append(directives, "sandbox "+strings.Join(csp.Sandbox, " ")) | ||||
| 	} | ||||
| 	if len(csp.ReportURI) > 0 { | ||||
| 		addDirective("report-uri", csp.ReportURI) | ||||
| 	} | ||||
| 	if len(csp.ReportTo) > 0 { | ||||
| 		addDirective("report-to", csp.ReportTo) | ||||
| 	} | ||||
| 	if csp.UpgradeInsecureRequests { | ||||
| 		directives = append(directives, "upgrade-insecure-requests") | ||||
| 	} | ||||
| 	if csp.BlockAllMixedContent { | ||||
| 		directives = append(directives, "block-all-mixed-content") | ||||
| 	} | ||||
|  | ||||
| 	headerValue := strings.Join(directives, "; ") | ||||
| 	return []string{"Content-Security-Policy", headerValue} | ||||
| } | ||||
|  | ||||
| // InjectContentSecurityPolicyHeader injects the CSP header into the response | ||||
| func InjectContentSecurityPolicyHeader(w http.ResponseWriter, csp *ContentSecurityPolicy) { | ||||
| 	if csp == nil || w.Header().Get("Content-Security-Policy") != "" { | ||||
| 		return | ||||
| 	} | ||||
| 	headerKV := csp.ToHeader() | ||||
| 	w.Header().Set(headerKV[0], headerKV[1]) | ||||
| } | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" | ||||
| 	"imuslab.com/zoraxy/mod/utils" | ||||
| ) | ||||
|  | ||||
| /* | ||||
| @@ -105,3 +106,49 @@ func (router *Router) RemoveProxyEndpointByRootname(rootnameOrMatchingDomain str | ||||
|  | ||||
| 	return targetEpt.Remove() | ||||
| } | ||||
|  | ||||
| // GetProxyEndpointById retrieves a proxy endpoint by its ID from the Router's ProxyEndpoints map. | ||||
| // It returns the ProxyEndpoint if found, or an error if not found. | ||||
| func (h *Router) GetProxyEndpointById(searchingDomain string, includeAlias bool) (*ProxyEndpoint, error) { | ||||
| 	var found *ProxyEndpoint | ||||
| 	h.ProxyEndpoints.Range(func(key, value interface{}) bool { | ||||
| 		proxy, ok := value.(*ProxyEndpoint) | ||||
| 		if ok && (proxy.RootOrMatchingDomain == searchingDomain || (includeAlias && utils.StringInArray(proxy.MatchingDomainAlias, searchingDomain))) { | ||||
| 			found = proxy | ||||
| 			return false // stop iteration | ||||
| 		} | ||||
| 		return true // continue iteration | ||||
| 	}) | ||||
| 	if found != nil { | ||||
| 		return found, nil | ||||
| 	} | ||||
| 	return nil, errors.New("proxy rule with given id not found") | ||||
| } | ||||
|  | ||||
| func (h *Router) GetProxyEndpointByAlias(alias string) (*ProxyEndpoint, error) { | ||||
| 	var found *ProxyEndpoint | ||||
| 	h.ProxyEndpoints.Range(func(key, value interface{}) bool { | ||||
| 		proxy, ok := value.(*ProxyEndpoint) | ||||
| 		if !ok { | ||||
| 			return true | ||||
| 		} | ||||
| 		//Also check for wildcard aliases that matches the alias | ||||
| 		for _, thisAlias := range proxy.MatchingDomainAlias { | ||||
| 			if ok && thisAlias == alias { | ||||
| 				found = proxy | ||||
| 				return false // stop iteration | ||||
| 			} else if ok && strings.HasPrefix(thisAlias, "*") { | ||||
| 				//Check if the alias matches a wildcard alias | ||||
| 				if strings.HasSuffix(alias, thisAlias[1:]) { | ||||
| 					found = proxy | ||||
| 					return false // stop iteration | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		return true // continue iteration | ||||
| 	}) | ||||
| 	if found != nil { | ||||
| 		return found, nil | ||||
| 	} | ||||
| 	return nil, errors.New("proxy rule with given alias not found") | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
| @@ -175,7 +179,8 @@ type ProxyEndpoint struct { | ||||
| 	Disabled             bool                    //If the rule is disabled | ||||
|  | ||||
| 	//Inbound TLS/SSL Related | ||||
| 	BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil) | ||||
| 	BypassGlobalTLS bool                             //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil) | ||||
| 	TlsOptions      *tlscert.HostSpecificTlsBehavior //TLS options for this endpoint, if nil, use global TLS options | ||||
|  | ||||
| 	//Virtual Directories | ||||
| 	VirtualDirectories []*VirtualDirectoryEndpoint | ||||
|   | ||||
| @@ -17,7 +17,7 @@ import ( | ||||
| type SniffResult int | ||||
|  | ||||
| const ( | ||||
| 	SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress | ||||
| 	SniffResultAccept SniffResult = iota // Forward the request to this plugin dynamic capture ingress | ||||
| 	SniffResultSkip                      // Skip this plugin and let the next plugin handle the request | ||||
| ) | ||||
|  | ||||
| @@ -62,7 +62,7 @@ func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http | ||||
| 		payload.rawRequest = r | ||||
|  | ||||
| 		sniffResult := handler(&payload) | ||||
| 		if sniffResult == SniffResultAccpet { | ||||
| 		if sniffResult == SniffResultAccept { | ||||
| 			w.WriteHeader(http.StatusOK) | ||||
| 			w.Write([]byte("OK")) | ||||
| 		} else { | ||||
|   | ||||
| @@ -47,15 +47,19 @@ func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) { | ||||
|  | ||||
| 	useTCP, _ := utils.PostBool(r, "useTCP") | ||||
| 	useUDP, _ := utils.PostBool(r, "useUDP") | ||||
| 	useProxyProtocol, _ := utils.PostBool(r, "useProxyProtocol") | ||||
| 	enableLogging, _ := utils.PostBool(r, "enableLogging") | ||||
|  | ||||
| 	//Create the target config | ||||
| 	newConfigUUID := m.NewConfig(&ProxyRelayOptions{ | ||||
| 		Name:          name, | ||||
| 		ListeningAddr: strings.TrimSpace(listenAddr), | ||||
| 		ProxyAddr:     strings.TrimSpace(proxyAddr), | ||||
| 		Timeout:       timeout, | ||||
| 		UseTCP:        useTCP, | ||||
| 		UseUDP:        useUDP, | ||||
| 		Name:             name, | ||||
| 		ListeningAddr:    strings.TrimSpace(listenAddr), | ||||
| 		ProxyAddr:        strings.TrimSpace(proxyAddr), | ||||
| 		Timeout:          timeout, | ||||
| 		UseTCP:           useTCP, | ||||
| 		UseUDP:           useUDP, | ||||
| 		UseProxyProtocol: useProxyProtocol, | ||||
| 		EnableLogging:    enableLogging, | ||||
| 	}) | ||||
|  | ||||
| 	js, _ := json.Marshal(newConfigUUID) | ||||
| @@ -75,6 +79,8 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request) | ||||
| 	proxyAddr, _ := utils.PostPara(r, "proxyAddr") | ||||
| 	useTCP, _ := utils.PostBool(r, "useTCP") | ||||
| 	useUDP, _ := utils.PostBool(r, "useUDP") | ||||
| 	useProxyProtocol, _ := utils.PostBool(r, "useProxyProtocol") | ||||
| 	enableLogging, _ := utils.PostBool(r, "enableLogging") | ||||
|  | ||||
| 	newTimeoutStr, _ := utils.PostPara(r, "timeout") | ||||
| 	newTimeout := -1 | ||||
| @@ -86,8 +92,21 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Create a new ProxyRuleUpdateConfig with the extracted parameters | ||||
| 	newConfig := &ProxyRuleUpdateConfig{ | ||||
| 		InstanceUUID:     configUUID, | ||||
| 		NewName:          newName, | ||||
| 		NewListeningAddr: listenAddr, | ||||
| 		NewProxyAddr:     proxyAddr, | ||||
| 		UseTCP:           useTCP, | ||||
| 		UseUDP:           useUDP, | ||||
| 		UseProxyProtocol: useProxyProtocol, | ||||
| 		EnableLogging:    enableLogging, | ||||
| 		NewTimeout:       newTimeout, | ||||
| 	} | ||||
|  | ||||
| 	// Call the EditConfig method to modify the configuration | ||||
| 	err = m.EditConfig(configUUID, newName, listenAddr, proxyAddr, useTCP, useUDP, newTimeout) | ||||
| 	err = m.EditConfig(newConfig) | ||||
| 	if err != nil { | ||||
| 		utils.SendErrorResponse(w, err.Error()) | ||||
| 		return | ||||
|   | ||||
							
								
								
									
										110
									
								
								src/mod/streamproxy/instances.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/mod/streamproxy/instances.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| package streamproxy | ||||
|  | ||||
| /* | ||||
| 	Instances.go | ||||
|  | ||||
| 	This file contains the methods to start, stop, and manage the proxy relay instances. | ||||
|  | ||||
| */ | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"log" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func (c *ProxyRelayInstance) LogMsg(message string, originalError error) { | ||||
| 	if !c.EnableLogging { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if originalError != nil { | ||||
| 		log.Println(message, "error:", originalError) | ||||
| 	} else { | ||||
| 		log.Println(message) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Start a proxy if stopped | ||||
| func (c *ProxyRelayInstance) Start() error { | ||||
| 	if c.IsRunning() { | ||||
| 		c.Running = true | ||||
| 		return errors.New("proxy already running") | ||||
| 	} | ||||
|  | ||||
| 	// Create a stopChan to control the loop | ||||
| 	tcpStopChan := make(chan bool) | ||||
| 	udpStopChan := make(chan bool) | ||||
|  | ||||
| 	//Start the proxy service | ||||
| 	if c.UseUDP { | ||||
| 		c.udpStopChan = udpStopChan | ||||
| 		go func() { | ||||
| 			err := c.ForwardUDP(c.ListeningAddress, c.ProxyTargetAddr, udpStopChan) | ||||
| 			if err != nil { | ||||
| 				if !c.UseTCP { | ||||
| 					c.Running = false | ||||
| 					c.udpStopChan = nil | ||||
| 					c.parent.SaveConfigToDatabase() | ||||
| 				} | ||||
| 				c.parent.logf("[proto:udp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err) | ||||
| 			} | ||||
| 		}() | ||||
| 	} | ||||
|  | ||||
| 	if c.UseTCP { | ||||
| 		c.tcpStopChan = tcpStopChan | ||||
| 		go func() { | ||||
| 			//Default to transport mode | ||||
| 			err := c.Port2host(c.ListeningAddress, c.ProxyTargetAddr, tcpStopChan) | ||||
| 			if err != nil { | ||||
| 				c.Running = false | ||||
| 				c.tcpStopChan = nil | ||||
| 				c.parent.SaveConfigToDatabase() | ||||
| 				c.parent.logf("[proto:tcp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err) | ||||
| 			} | ||||
| 		}() | ||||
| 	} | ||||
|  | ||||
| 	//Successfully spawned off the proxy routine | ||||
| 	c.Running = true | ||||
| 	c.parent.SaveConfigToDatabase() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Return if a proxy config is running | ||||
| func (c *ProxyRelayInstance) IsRunning() bool { | ||||
| 	return c.tcpStopChan != nil || c.udpStopChan != nil | ||||
| } | ||||
|  | ||||
| // Restart a proxy config | ||||
| func (c *ProxyRelayInstance) Restart() { | ||||
| 	if c.IsRunning() { | ||||
| 		c.Stop() | ||||
| 	} | ||||
| 	time.Sleep(3000 * time.Millisecond) | ||||
| 	c.Start() | ||||
| } | ||||
|  | ||||
| // Stop a running proxy if running | ||||
| func (c *ProxyRelayInstance) Stop() { | ||||
| 	c.parent.logf("Stopping Stream Proxy "+c.Name, nil) | ||||
|  | ||||
| 	if c.udpStopChan != nil { | ||||
| 		c.parent.logf("Stopping UDP for "+c.Name, nil) | ||||
| 		c.udpStopChan <- true | ||||
| 		c.udpStopChan = nil | ||||
| 	} | ||||
|  | ||||
| 	if c.tcpStopChan != nil { | ||||
| 		c.parent.logf("Stopping TCP for "+c.Name, nil) | ||||
| 		c.tcpStopChan <- true | ||||
| 		c.tcpStopChan = nil | ||||
| 	} | ||||
|  | ||||
| 	c.parent.logf("Stopped Stream Proxy "+c.Name, nil) | ||||
| 	c.Running = false | ||||
|  | ||||
| 	//Update the running status | ||||
| 	c.parent.SaveConfigToDatabase() | ||||
| } | ||||
| @@ -8,7 +8,6 @@ import ( | ||||
| 	"path/filepath" | ||||
| 	"sync" | ||||
| 	"sync/atomic" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/google/uuid" | ||||
| 	"imuslab.com/zoraxy/mod/info/logger" | ||||
| @@ -24,24 +23,44 @@ import ( | ||||
| */ | ||||
|  | ||||
| type ProxyRelayOptions struct { | ||||
| 	Name          string | ||||
| 	ListeningAddr string | ||||
| 	ProxyAddr     string | ||||
| 	Timeout       int | ||||
| 	UseTCP        bool | ||||
| 	UseUDP        bool | ||||
| 	Name             string | ||||
| 	ListeningAddr    string | ||||
| 	ProxyAddr        string | ||||
| 	Timeout          int | ||||
| 	UseTCP           bool | ||||
| 	UseUDP           bool | ||||
| 	UseProxyProtocol bool | ||||
| 	EnableLogging    bool | ||||
| } | ||||
|  | ||||
| type ProxyRelayConfig struct { | ||||
| 	UUID                        string       //A UUIDv4 representing this config | ||||
| 	Name                        string       //Name of the config | ||||
| 	Running                     bool         //Status, read only | ||||
| 	AutoStart                   bool         //If the service suppose to started automatically | ||||
| 	ListeningAddress            string       //Listening Address, usually 127.0.0.1:port | ||||
| 	ProxyTargetAddr             string       //Proxy target address | ||||
| 	UseTCP                      bool         //Enable TCP proxy | ||||
| 	UseUDP                      bool         //Enable UDP proxy | ||||
| 	Timeout                     int          //Timeout for connection in sec | ||||
| // ProxyRuleUpdateConfig is used to update the proxy rule config | ||||
| type ProxyRuleUpdateConfig struct { | ||||
| 	InstanceUUID     string //The target instance UUID to update | ||||
| 	NewName          string //New name for the instance, leave empty for no change | ||||
| 	NewListeningAddr string //New listening address, leave empty for no change | ||||
| 	NewProxyAddr     string //New proxy target address, leave empty for no change | ||||
| 	UseTCP           bool   //Enable TCP proxy, default to false | ||||
| 	UseUDP           bool   //Enable UDP proxy, default to false | ||||
| 	UseProxyProtocol bool   //Enable Proxy Protocol, default to false | ||||
| 	EnableLogging    bool   //Enable Logging TCP/UDP Message, default to true | ||||
| 	NewTimeout       int    //New timeout for the connection, leave -1 for no change | ||||
| } | ||||
|  | ||||
| type ProxyRelayInstance struct { | ||||
| 	/* Runtime Config */ | ||||
| 	UUID             string //A UUIDv4 representing this config | ||||
| 	Name             string //Name of the config | ||||
| 	Running          bool   //Status, read only | ||||
| 	AutoStart        bool   //If the service suppose to started automatically | ||||
| 	ListeningAddress string //Listening Address, usually 127.0.0.1:port | ||||
| 	ProxyTargetAddr  string //Proxy target address | ||||
| 	UseTCP           bool   //Enable TCP proxy | ||||
| 	UseUDP           bool   //Enable UDP proxy | ||||
| 	UseProxyProtocol bool   //Enable Proxy Protocol | ||||
| 	EnableLogging    bool   //Enable logging for ProxyInstance | ||||
| 	Timeout          int    //Timeout for connection in sec | ||||
|  | ||||
| 	/* Internal */ | ||||
| 	tcpStopChan                 chan bool    //Stop channel for TCP listener | ||||
| 	udpStopChan                 chan bool    //Stop channel for UDP listener | ||||
| 	aTobAccumulatedByteTransfer atomic.Int64 //Accumulated byte transfer from A to B | ||||
| @@ -60,13 +79,14 @@ type Options struct { | ||||
| type Manager struct { | ||||
| 	//Config and stores | ||||
| 	Options *Options | ||||
| 	Configs []*ProxyRelayConfig | ||||
| 	Configs []*ProxyRelayInstance | ||||
|  | ||||
| 	//Realtime Statistics | ||||
| 	Connections int //currently connected connect counts | ||||
|  | ||||
| } | ||||
|  | ||||
| // NewStreamProxy creates a new stream proxy manager with the given options | ||||
| func NewStreamProxy(options *Options) (*Manager, error) { | ||||
| 	if !utils.FileExists(options.ConfigStore) { | ||||
| 		err := os.MkdirAll(options.ConfigStore, 0775) | ||||
| @@ -76,7 +96,7 @@ func NewStreamProxy(options *Options) (*Manager, error) { | ||||
| 	} | ||||
|  | ||||
| 	//Load relay configs from db | ||||
| 	previousRules := []*ProxyRelayConfig{} | ||||
| 	previousRules := []*ProxyRelayInstance{} | ||||
| 	streamProxyConfigFiles, err := filepath.Glob(options.ConfigStore + "/*.config") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| @@ -89,7 +109,7 @@ func NewStreamProxy(options *Options) (*Manager, error) { | ||||
| 			options.Logger.PrintAndLog("stream-prox", "Read stream proxy config failed", err) | ||||
| 			continue | ||||
| 		} | ||||
| 		thisRelayConfig := &ProxyRelayConfig{} | ||||
| 		thisRelayConfig := &ProxyRelayInstance{} | ||||
| 		err = json.Unmarshal(configBytes, thisRelayConfig) | ||||
| 		if err != nil { | ||||
| 			options.Logger.PrintAndLog("stream-prox", "Unmarshal stream proxy config failed", err) | ||||
| @@ -142,6 +162,7 @@ func (m *Manager) logf(message string, originalError error) { | ||||
| 	m.Options.Logger.PrintAndLog("stream-prox", message, originalError) | ||||
| } | ||||
|  | ||||
| // NewConfig creates a new proxy relay config with the given options | ||||
| func (m *Manager) NewConfig(config *ProxyRelayOptions) string { | ||||
| 	//Generate two zero value for atomic int64 | ||||
| 	aAcc := atomic.Int64{} | ||||
| @@ -150,13 +171,15 @@ func (m *Manager) NewConfig(config *ProxyRelayOptions) string { | ||||
| 	bAcc.Store(0) | ||||
| 	//Generate a new config from options | ||||
| 	configUUID := uuid.New().String() | ||||
| 	thisConfig := ProxyRelayConfig{ | ||||
| 	thisConfig := ProxyRelayInstance{ | ||||
| 		UUID:                        configUUID, | ||||
| 		Name:                        config.Name, | ||||
| 		ListeningAddress:            config.ListeningAddr, | ||||
| 		ProxyTargetAddr:             config.ProxyAddr, | ||||
| 		UseTCP:                      config.UseTCP, | ||||
| 		UseUDP:                      config.UseUDP, | ||||
| 		UseProxyProtocol:            config.UseProxyProtocol, | ||||
| 		EnableLogging:               config.EnableLogging, | ||||
| 		Timeout:                     config.Timeout, | ||||
| 		tcpStopChan:                 nil, | ||||
| 		udpStopChan:                 nil, | ||||
| @@ -170,7 +193,7 @@ func (m *Manager) NewConfig(config *ProxyRelayOptions) string { | ||||
| 	return configUUID | ||||
| } | ||||
|  | ||||
| func (m *Manager) GetConfigByUUID(configUUID string) (*ProxyRelayConfig, error) { | ||||
| func (m *Manager) GetConfigByUUID(configUUID string) (*ProxyRelayInstance, error) { | ||||
| 	// Find and return the config with the specified UUID | ||||
| 	for _, config := range m.Configs { | ||||
| 		if config.UUID == configUUID { | ||||
| @@ -181,32 +204,34 @@ func (m *Manager) GetConfigByUUID(configUUID string) (*ProxyRelayConfig, error) | ||||
| } | ||||
|  | ||||
| // Edit the config based on config UUID, leave empty for unchange fields | ||||
| func (m *Manager) EditConfig(configUUID string, newName string, newListeningAddr string, newProxyAddr string, useTCP bool, useUDP bool, newTimeout int) error { | ||||
| func (m *Manager) EditConfig(newConfig *ProxyRuleUpdateConfig) error { | ||||
| 	// Find the config with the specified UUID | ||||
| 	foundConfig, err := m.GetConfigByUUID(configUUID) | ||||
| 	foundConfig, err := m.GetConfigByUUID(newConfig.InstanceUUID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Validate and update the fields | ||||
| 	if newName != "" { | ||||
| 		foundConfig.Name = newName | ||||
| 	if newConfig.NewName != "" { | ||||
| 		foundConfig.Name = newConfig.NewName | ||||
| 	} | ||||
| 	if newListeningAddr != "" { | ||||
| 		foundConfig.ListeningAddress = newListeningAddr | ||||
| 	if newConfig.NewListeningAddr != "" { | ||||
| 		foundConfig.ListeningAddress = newConfig.NewListeningAddr | ||||
| 	} | ||||
| 	if newProxyAddr != "" { | ||||
| 		foundConfig.ProxyTargetAddr = newProxyAddr | ||||
| 	if newConfig.NewProxyAddr != "" { | ||||
| 		foundConfig.ProxyTargetAddr = newConfig.NewProxyAddr | ||||
| 	} | ||||
|  | ||||
| 	foundConfig.UseTCP = useTCP | ||||
| 	foundConfig.UseUDP = useUDP | ||||
| 	foundConfig.UseTCP = newConfig.UseTCP | ||||
| 	foundConfig.UseUDP = newConfig.UseUDP | ||||
| 	foundConfig.UseProxyProtocol = newConfig.UseProxyProtocol | ||||
| 	foundConfig.EnableLogging = newConfig.EnableLogging | ||||
|  | ||||
| 	if newTimeout != -1 { | ||||
| 		if newTimeout < 0 { | ||||
| 	if newConfig.NewTimeout != -1 { | ||||
| 		if newConfig.NewTimeout < 0 { | ||||
| 			return errors.New("invalid timeout value given") | ||||
| 		} | ||||
| 		foundConfig.Timeout = newTimeout | ||||
| 		foundConfig.Timeout = newConfig.NewTimeout | ||||
| 	} | ||||
|  | ||||
| 	m.SaveConfigToDatabase() | ||||
| @@ -215,12 +240,11 @@ func (m *Manager) EditConfig(configUUID string, newName string, newListeningAddr | ||||
| 	if foundConfig.IsRunning() { | ||||
| 		foundConfig.Restart() | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Remove the config from file by UUID | ||||
| func (m *Manager) RemoveConfig(configUUID string) error { | ||||
| 	//Remove the config from file | ||||
| 	err := os.Remove(filepath.Join(m.Options.ConfigStore, configUUID+".config")) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| @@ -250,91 +274,3 @@ func (m *Manager) SaveConfigToDatabase() { | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /* | ||||
| 	Config Functions | ||||
| */ | ||||
|  | ||||
| // Start a proxy if stopped | ||||
| func (c *ProxyRelayConfig) Start() error { | ||||
| 	if c.IsRunning() { | ||||
| 		c.Running = true | ||||
| 		return errors.New("proxy already running") | ||||
| 	} | ||||
|  | ||||
| 	// Create a stopChan to control the loop | ||||
| 	tcpStopChan := make(chan bool) | ||||
| 	udpStopChan := make(chan bool) | ||||
|  | ||||
| 	//Start the proxy service | ||||
| 	if c.UseUDP { | ||||
| 		c.udpStopChan = udpStopChan | ||||
| 		go func() { | ||||
| 			err := c.ForwardUDP(c.ListeningAddress, c.ProxyTargetAddr, udpStopChan) | ||||
| 			if err != nil { | ||||
| 				if !c.UseTCP { | ||||
| 					c.Running = false | ||||
| 					c.udpStopChan = nil | ||||
| 					c.parent.SaveConfigToDatabase() | ||||
| 				} | ||||
| 				c.parent.logf("[proto:udp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err) | ||||
| 			} | ||||
| 		}() | ||||
| 	} | ||||
|  | ||||
| 	if c.UseTCP { | ||||
| 		c.tcpStopChan = tcpStopChan | ||||
| 		go func() { | ||||
| 			//Default to transport mode | ||||
| 			err := c.Port2host(c.ListeningAddress, c.ProxyTargetAddr, tcpStopChan) | ||||
| 			if err != nil { | ||||
| 				c.Running = false | ||||
| 				c.tcpStopChan = nil | ||||
| 				c.parent.SaveConfigToDatabase() | ||||
| 				c.parent.logf("[proto:tcp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err) | ||||
| 			} | ||||
| 		}() | ||||
| 	} | ||||
|  | ||||
| 	//Successfully spawned off the proxy routine | ||||
| 	c.Running = true | ||||
| 	c.parent.SaveConfigToDatabase() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Return if a proxy config is running | ||||
| func (c *ProxyRelayConfig) IsRunning() bool { | ||||
| 	return c.tcpStopChan != nil || c.udpStopChan != nil | ||||
| } | ||||
|  | ||||
| // Restart a proxy config | ||||
| func (c *ProxyRelayConfig) Restart() { | ||||
| 	if c.IsRunning() { | ||||
| 		c.Stop() | ||||
| 	} | ||||
| 	time.Sleep(3000 * time.Millisecond) | ||||
| 	c.Start() | ||||
| } | ||||
|  | ||||
| // Stop a running proxy if running | ||||
| func (c *ProxyRelayConfig) Stop() { | ||||
| 	c.parent.logf("Stopping Stream Proxy "+c.Name, nil) | ||||
|  | ||||
| 	if c.udpStopChan != nil { | ||||
| 		c.parent.logf("Stopping UDP for "+c.Name, nil) | ||||
| 		c.udpStopChan <- true | ||||
| 		c.udpStopChan = nil | ||||
| 	} | ||||
|  | ||||
| 	if c.tcpStopChan != nil { | ||||
| 		c.parent.logf("Stopping TCP for "+c.Name, nil) | ||||
| 		c.tcpStopChan <- true | ||||
| 		c.tcpStopChan = nil | ||||
| 	} | ||||
|  | ||||
| 	c.parent.logf("Stopped Stream Proxy "+c.Name, nil) | ||||
| 	c.Running = false | ||||
|  | ||||
| 	//Update the running status | ||||
| 	c.parent.SaveConfigToDatabase() | ||||
| } | ||||
|   | ||||
| @@ -12,7 +12,7 @@ func TestPort2Port(t *testing.T) { | ||||
| 	stopChan := make(chan bool) | ||||
|  | ||||
| 	// Create a ProxyRelayConfig with dummy values | ||||
| 	config := &streamproxy.ProxyRelayConfig{ | ||||
| 	config := &streamproxy.ProxyRelayInstance{ | ||||
| 		Timeout: 1, | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package streamproxy | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net" | ||||
| @@ -30,48 +31,67 @@ func isValidPort(port string) bool { | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| func connCopy(conn1 net.Conn, conn2 net.Conn, wg *sync.WaitGroup, accumulator *atomic.Int64) { | ||||
| func (c *ProxyRelayInstance) connCopy(conn1 net.Conn, conn2 net.Conn, wg *sync.WaitGroup, accumulator *atomic.Int64) { | ||||
| 	n, err := io.Copy(conn1, conn2) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	accumulator.Add(n) //Add to accumulator | ||||
| 	conn1.Close() | ||||
| 	log.Println("[←]", "close the connect at local:["+conn1.LocalAddr().String()+"] and remote:["+conn1.RemoteAddr().String()+"]") | ||||
| 	c.LogMsg("[←] close the connect at local:["+conn1.LocalAddr().String()+"] and remote:["+conn1.RemoteAddr().String()+"]", nil) | ||||
| 	//conn2.Close() | ||||
| 	//log.Println("[←]", "close the connect at local:["+conn2.LocalAddr().String()+"] and remote:["+conn2.RemoteAddr().String()+"]") | ||||
| 	//c.LogMsg("[←] close the connect at local:["+conn2.LocalAddr().String()+"] and remote:["+conn2.RemoteAddr().String()+"]", nil) | ||||
| 	wg.Done() | ||||
| } | ||||
|  | ||||
| func forward(conn1 net.Conn, conn2 net.Conn, aTob *atomic.Int64, bToa *atomic.Int64) { | ||||
| 	log.Printf("[+] start transmit. [%s],[%s] <-> [%s],[%s] \n", conn1.LocalAddr().String(), conn1.RemoteAddr().String(), conn2.LocalAddr().String(), conn2.RemoteAddr().String()) | ||||
| func writeProxyProtocolHeaderV1(dst net.Conn, src net.Conn) error { | ||||
| 	clientAddr, ok1 := src.RemoteAddr().(*net.TCPAddr) | ||||
| 	proxyAddr, ok2 := src.LocalAddr().(*net.TCPAddr) | ||||
| 	if !ok1 || !ok2 { | ||||
| 		return errors.New("invalid TCP address for proxy protocol") | ||||
| 	} | ||||
|  | ||||
| 	header := fmt.Sprintf("PROXY TCP4 %s %s %d %d\r\n", | ||||
| 		clientAddr.IP.String(), | ||||
| 		proxyAddr.IP.String(), | ||||
| 		clientAddr.Port, | ||||
| 		proxyAddr.Port) | ||||
|  | ||||
| 	_, err := dst.Write([]byte(header)) | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func (c *ProxyRelayInstance) forward(conn1 net.Conn, conn2 net.Conn, aTob *atomic.Int64, bToa *atomic.Int64) { | ||||
| 	msg := fmt.Sprintf("[+] start transmit. [%s],[%s] <-> [%s],[%s]", | ||||
| 		conn1.LocalAddr().String(), conn1.RemoteAddr().String(), | ||||
| 		conn2.LocalAddr().String(), conn2.RemoteAddr().String()) | ||||
| 	c.LogMsg(msg, nil) | ||||
|  | ||||
| 	var wg sync.WaitGroup | ||||
| 	// wait tow goroutines | ||||
| 	wg.Add(2) | ||||
| 	go connCopy(conn1, conn2, &wg, aTob) | ||||
| 	go connCopy(conn2, conn1, &wg, bToa) | ||||
| 	//blocking when the wg is locked | ||||
| 	go c.connCopy(conn1, conn2, &wg, aTob) | ||||
| 	go c.connCopy(conn2, conn1, &wg, bToa) | ||||
| 	wg.Wait() | ||||
| } | ||||
|  | ||||
| func (c *ProxyRelayConfig) accept(listener net.Listener) (net.Conn, error) { | ||||
| func (c *ProxyRelayInstance) accept(listener net.Listener) (net.Conn, error) { | ||||
| 	conn, err := listener.Accept() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	//Check if connection in blacklist or whitelist | ||||
| 	// Check if connection in blacklist or whitelist | ||||
| 	if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok { | ||||
| 		if !c.parent.Options.AccessControlHandler(conn) { | ||||
| 			time.Sleep(300 * time.Millisecond) | ||||
| 			conn.Close() | ||||
| 			log.Println("[x]", "Connection from "+addr.IP.String()+" rejected by access control policy") | ||||
| 			c.LogMsg("[x] Connection from "+addr.IP.String()+" rejected by access control policy", nil) | ||||
| 			return nil, errors.New("Connection from " + addr.IP.String() + " rejected by access control policy") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	log.Println("[√]", "accept a new client. remote address:["+conn.RemoteAddr().String()+"], local address:["+conn.LocalAddr().String()+"]") | ||||
| 	return conn, err | ||||
| 	c.LogMsg("[√] accept a new client. remote address:["+conn.RemoteAddr().String()+"], local address:["+conn.LocalAddr().String()+"]", nil) | ||||
| 	return conn, nil | ||||
| } | ||||
|  | ||||
| func startListener(address string) (net.Listener, error) { | ||||
| @@ -92,7 +112,7 @@ func startListener(address string) (net.Listener, error) { | ||||
| portA -> server | ||||
| server -> portB | ||||
| */ | ||||
| func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, stopChan chan bool) error { | ||||
| func (c *ProxyRelayInstance) Port2host(allowPort string, targetAddress string, stopChan chan bool) error { | ||||
| 	listenerStartingAddr := allowPort | ||||
| 	if isValidPort(allowPort) { | ||||
| 		//number only, e.g. 8080 | ||||
| @@ -112,7 +132,7 @@ func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, sto | ||||
| 	//Start stop handler | ||||
| 	go func() { | ||||
| 		<-stopChan | ||||
| 		log.Println("[x]", "Received stop signal. Exiting Port to Host forwarder") | ||||
| 		c.LogMsg("[x] Received stop signal. Exiting Port to Host forwarder", nil) | ||||
| 		server.Close() | ||||
| 	}() | ||||
|  | ||||
| @@ -129,18 +149,32 @@ func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, sto | ||||
| 		} | ||||
|  | ||||
| 		go func(targetAddress string) { | ||||
| 			log.Println("[+]", "start connect host:["+targetAddress+"]") | ||||
| 			c.LogMsg("[+] start connect host:["+targetAddress+"]", nil) | ||||
| 			target, err := net.Dial("tcp", targetAddress) | ||||
| 			if err != nil { | ||||
| 				// temporarily unavailable, don't use fatal. | ||||
| 				log.Println("[x]", "connect target address ["+targetAddress+"] faild. retry in ", c.Timeout, "seconds. ") | ||||
| 				c.LogMsg("[x] connect target address ["+targetAddress+"] failed. retry in "+strconv.Itoa(c.Timeout)+" seconds.", nil) | ||||
| 				conn.Close() | ||||
| 				log.Println("[←]", "close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]") | ||||
| 				c.LogMsg("[←] close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]", nil) | ||||
| 				time.Sleep(time.Duration(c.Timeout) * time.Second) | ||||
| 				return | ||||
| 			} | ||||
| 			log.Println("[→]", "connect target address ["+targetAddress+"] success.") | ||||
| 			forward(target, conn, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer) | ||||
| 			c.LogMsg("[→] connect target address ["+targetAddress+"] success.", nil) | ||||
|  | ||||
| 			if c.UseProxyProtocol { | ||||
| 				c.LogMsg("[+] write proxy protocol header to target address ["+targetAddress+"]", nil) | ||||
| 				err = writeProxyProtocolHeaderV1(target, conn) | ||||
| 				if err != nil { | ||||
| 					c.LogMsg("[x] Write proxy protocol header failed: "+err.Error(), nil) | ||||
| 					target.Close() | ||||
| 					conn.Close() | ||||
| 					c.LogMsg("[←] close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]", nil) | ||||
| 					time.Sleep(time.Duration(c.Timeout) * time.Second) | ||||
| 					return | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			c.forward(target, conn, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer) | ||||
| 		}(targetAddress) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -53,7 +53,7 @@ func initUDPConnections(listenAddr string, targetAddress string) (*net.UDPConn, | ||||
| } | ||||
|  | ||||
| // Go routine which manages connection from server to single client | ||||
| func (c *ProxyRelayConfig) RunUDPConnectionRelay(conn *udpClientServerConn, lisenter *net.UDPConn) { | ||||
| func (c *ProxyRelayInstance) RunUDPConnectionRelay(conn *udpClientServerConn, lisenter *net.UDPConn) { | ||||
| 	var buffer [1500]byte | ||||
| 	for { | ||||
| 		// Read from server | ||||
| @@ -74,7 +74,7 @@ func (c *ProxyRelayConfig) RunUDPConnectionRelay(conn *udpClientServerConn, lise | ||||
| } | ||||
|  | ||||
| // Close all connections that waiting for read from server | ||||
| func (c *ProxyRelayConfig) CloseAllUDPConnections() { | ||||
| func (c *ProxyRelayInstance) CloseAllUDPConnections() { | ||||
| 	c.udpClientMap.Range(func(clientAddr, clientServerConn interface{}) bool { | ||||
| 		conn := clientServerConn.(*udpClientServerConn) | ||||
| 		conn.ServerConn.Close() | ||||
| @@ -82,7 +82,7 @@ func (c *ProxyRelayConfig) CloseAllUDPConnections() { | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (c *ProxyRelayConfig) ForwardUDP(address1, address2 string, stopChan chan bool) error { | ||||
| func (c *ProxyRelayInstance) ForwardUDP(address1, address2 string, stopChan chan bool) error { | ||||
| 	//By default the incoming listen Address is int | ||||
| 	//We need to add the loopback address into it | ||||
| 	if isValidPort(address1) { | ||||
| @@ -138,12 +138,12 @@ func (c *ProxyRelayConfig) ForwardUDP(address1, address2 string, stopChan chan b | ||||
| 				continue | ||||
| 			} | ||||
| 			c.udpClientMap.Store(saddr, conn) | ||||
| 			log.Println("[UDP] Created new connection for client " + saddr) | ||||
| 			c.LogMsg("[UDP] Created new connection for client "+saddr, nil) | ||||
| 			// Fire up routine to manage new connection | ||||
| 			go c.RunUDPConnectionRelay(conn, lisener) | ||||
|  | ||||
| 		} else { | ||||
| 			log.Println("[UDP] Found connection for client " + saddr) | ||||
| 			c.LogMsg("[UDP] Found connection for client "+saddr, nil) | ||||
| 			conn = rawConn.(*udpClientServerConn) | ||||
| 		} | ||||
|  | ||||
|   | ||||
							
								
								
									
										93
									
								
								src/mod/tlscert/certgen.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/mod/tlscert/certgen.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										352
									
								
								src/mod/tlscert/handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										352
									
								
								src/mod/tlscert/handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -20,17 +20,26 @@ type CertCache struct { | ||||
| 	PriKey string | ||||
| } | ||||
|  | ||||
| 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             map[string]string //Preferred certificate for this server name, if empty, use the first matching certificate | ||||
| } | ||||
|  | ||||
| type Manager struct { | ||||
| 	CertStore   string         //Path where all the certs are stored | ||||
| 	LoadedCerts []*CertCache   //A list of loaded certs | ||||
| 	Logger      *logger.Logger //System wide logger for debug mesage | ||||
| 	verbal      bool | ||||
|  | ||||
| 	/* External handlers */ | ||||
| 	hostSpecificTlsBehavior func(serverName string) (*HostSpecificTlsBehavior, error) // Function to get host specific TLS behavior, if nil, use global TLS options | ||||
| } | ||||
|  | ||||
| //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) | ||||
| 	} | ||||
| @@ -50,10 +59,10 @@ func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager, | ||||
| 	} | ||||
|  | ||||
| 	thisManager := Manager{ | ||||
| 		CertStore:   certStore, | ||||
| 		LoadedCerts: []*CertCache{}, | ||||
| 		verbal:      verbal, | ||||
| 		Logger:      logger, | ||||
| 		CertStore:               certStore, | ||||
| 		LoadedCerts:             []*CertCache{}, | ||||
| 		hostSpecificTlsBehavior: defaultHostSpecificTlsBehavior, //Default to no SNI and no auto HTTPS | ||||
| 		Logger:                  logger, | ||||
| 	} | ||||
|  | ||||
| 	err := thisManager.UpdateLoadedCertList() | ||||
| @@ -64,6 +73,25 @@ func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager, | ||||
| 	return &thisManager, nil | ||||
| } | ||||
|  | ||||
| // Default host specific TLS behavior | ||||
| // This is used when no specific TLS behavior is defined for a server name | ||||
| func GetDefaultHostSpecificTlsBehavior() *HostSpecificTlsBehavior { | ||||
| 	return &HostSpecificTlsBehavior{ | ||||
| 		DisableSNI:                       false, | ||||
| 		DisableLegacyCertificateMatching: false, | ||||
| 		EnableAutoHTTPS:                  false, | ||||
| 		PreferredCertificate:             map[string]string{}, // No preferred certificate, use the first matching certificate | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func defaultHostSpecificTlsBehavior(serverName string) (*HostSpecificTlsBehavior, error) { | ||||
| 	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 | ||||
| @@ -161,24 +189,11 @@ func (m *Manager) ListCerts() ([]string, error) { | ||||
|  | ||||
| // Get a certificate from disk where its certificate matches with the helloinfo | ||||
| func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) { | ||||
| 	//Check if the domain corrisponding cert exists | ||||
| 	pubKey := "./tmp/localhost.pem" | ||||
| 	priKey := "./tmp/localhost.key" | ||||
|  | ||||
| 	if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".pem")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) { | ||||
| 		//Direct hit | ||||
| 		pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".pem") | ||||
| 		priKey = filepath.Join(m.CertStore, helloInfo.ServerName+".key") | ||||
| 	} else if m.CertMatchExists(helloInfo.ServerName) { | ||||
| 		//Use x509 | ||||
| 		pubKey, priKey = m.GetCertByX509CNHostname(helloInfo.ServerName) | ||||
| 	} else { | ||||
| 		//Fallback to legacy method of matching certificates | ||||
| 		if m.DefaultCertExists() { | ||||
| 			//Use default.pem and default.key | ||||
| 			pubKey = filepath.Join(m.CertStore, "default.pem") | ||||
| 			priKey = filepath.Join(m.CertStore, "default.key") | ||||
| 		} | ||||
| 	//Look for the certificate by hostname | ||||
| 	pubKey, priKey, err := m.GetCertificateByHostname(helloInfo.ServerName) | ||||
| 	if err != nil { | ||||
| 		m.Logger.PrintAndLog("tls-router", "Failed to get certificate for "+helloInfo.ServerName, err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	//Load the cert and serve it | ||||
| @@ -190,6 +205,55 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err | ||||
| 	return &cer, nil | ||||
| } | ||||
|  | ||||
| // GetCertificateByHostname returns the certificate and private key for a given hostname | ||||
| func (m *Manager) GetCertificateByHostname(hostname string) (string, string, error) { | ||||
| 	//Check if the domain corrisponding cert exists | ||||
| 	pubKey := "./tmp/localhost.pem" | ||||
| 	priKey := "./tmp/localhost.key" | ||||
|  | ||||
| 	tlsBehavior, err := m.hostSpecificTlsBehavior(hostname) | ||||
| 	if err != nil { | ||||
| 		tlsBehavior, _ = defaultHostSpecificTlsBehavior(hostname) | ||||
| 	} | ||||
| 	preferredCertificate, ok := tlsBehavior.PreferredCertificate[hostname] | ||||
| 	if !ok { | ||||
| 		preferredCertificate = "" | ||||
| 	} | ||||
|  | ||||
| 	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, preferredCertificate+".pem") | ||||
| 		priKey = filepath.Join(m.CertStore, preferredCertificate+".key") | ||||
| 	} else { | ||||
| 		if !tlsBehavior.DisableLegacyCertificateMatching && | ||||
| 			utils.FileExists(filepath.Join(m.CertStore, hostname+".pem")) && | ||||
| 			utils.FileExists(filepath.Join(m.CertStore, hostname+".key")) { | ||||
| 			//Legacy filename matching, use the file names directly | ||||
| 			//This is the legacy method of matching certificates, it will match the file names directly | ||||
| 			//This is used for compatibility with Zoraxy v2 setups | ||||
| 			pubKey = filepath.Join(m.CertStore, hostname+".pem") | ||||
| 			priKey = filepath.Join(m.CertStore, hostname+".key") | ||||
| 		} else if !tlsBehavior.DisableSNI && | ||||
| 			m.CertMatchExists(hostname) { | ||||
| 			//SNI scan match, find the first matching certificate | ||||
| 			pubKey, priKey = m.GetCertByX509CNHostname(hostname) | ||||
| 		} else if tlsBehavior.EnableAutoHTTPS { | ||||
| 			//Get certificate from CA, WIP | ||||
| 			//TODO: Implement AutoHTTPS | ||||
| 		} else { | ||||
| 			//Fallback to legacy method of matching certificates | ||||
| 			if m.DefaultCertExists() { | ||||
| 				//Use default.pem and default.key | ||||
| 				pubKey = filepath.Join(m.CertStore, "default.pem") | ||||
| 				priKey = filepath.Join(m.CertStore, "default.key") | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return pubKey, priKey, nil | ||||
| } | ||||
|  | ||||
| // Check if both the default cert public key and private key exists | ||||
| func (m *Manager) DefaultCertExists() bool { | ||||
| 	return utils.FileExists(filepath.Join(m.CertStore, "default.pem")) && utils.FileExists(filepath.Join(m.CertStore, "default.key")) | ||||
| @@ -220,7 +284,6 @@ func (m *Manager) RemoveCert(domain string) error { | ||||
|  | ||||
| 	//Update the cert list | ||||
| 	m.UpdateLoadedCertList() | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -69,6 +69,12 @@ func (ws *WebServer) HandlePortChange(w http.ResponseWriter, r *http.Request) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Check if newPort is a valid TCP port number (1-65535) | ||||
| 	if newPort < 1 || newPort > 65535 { | ||||
| 		utils.SendErrorResponse(w, "invalid port number given") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	err = ws.ChangePort(strconv.Itoa(newPort)) | ||||
| 	if err != nil { | ||||
| 		utils.SendErrorResponse(w, err.Error()) | ||||
| @@ -106,6 +112,17 @@ func (ws *WebServer) SetDisableListenToAllInterface(w http.ResponseWriter, r *ht | ||||
| 		utils.SendErrorResponse(w, "unable to save setting") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Update the option in the web server instance | ||||
| 	ws.option.DisableListenToAllInterface = disableListen | ||||
|  | ||||
| 	// If the server is running and the setting is changed, we need to restart the server | ||||
| 	if ws.IsRunning() { | ||||
| 		err = ws.Restart() | ||||
| 		if err != nil { | ||||
| 			utils.SendErrorResponse(w, "unable to restart web server: "+err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	utils.SendOK(w) | ||||
| } | ||||
|   | ||||
| @@ -210,6 +210,27 @@ func (ws *WebServer) Stop() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (ws *WebServer) Restart() error { | ||||
| 	if ws.isRunning { | ||||
| 		if err := ws.Stop(); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err := ws.Start(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	ws.option.Logger.PrintAndLog("static-webserv", "Static Web Server restarted. Listening on :"+ws.option.Port, nil) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (ws *WebServer) IsRunning() bool { | ||||
| 	ws.mu.Lock() | ||||
| 	defer ws.mu.Unlock() | ||||
| 	return ws.isRunning | ||||
| } | ||||
|  | ||||
| // UpdateDirectoryListing enables or disables directory listing. | ||||
| func (ws *WebServer) UpdateDirectoryListing(enable bool) { | ||||
| 	ws.option.EnableDirectoryListing = enable | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import ( | ||||
| 	"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" | ||||
| 	"imuslab.com/zoraxy/mod/dynamicproxy/rewrite" | ||||
| 	"imuslab.com/zoraxy/mod/netutils" | ||||
| 	"imuslab.com/zoraxy/mod/tlscert" | ||||
| 	"imuslab.com/zoraxy/mod/uptime" | ||||
| 	"imuslab.com/zoraxy/mod/utils" | ||||
| ) | ||||
| @@ -134,12 +135,12 @@ func ReverseProxtInit() { | ||||
| 		Load all conf from files | ||||
|  | ||||
| 	*/ | ||||
| 	confs, _ := filepath.Glob("./conf/proxy/*.config") | ||||
| 	confs, _ := filepath.Glob(CONF_HTTP_PROXY + "/*.config") | ||||
| 	for _, conf := range confs { | ||||
| 		err := LoadReverseProxyConfig(conf) | ||||
| 		if err != nil { | ||||
| 			SystemWideLogger.PrintAndLog("proxy-config", "Failed to load config file: "+filepath.Base(conf), err) | ||||
| 			return | ||||
| 			continue | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -334,7 +335,8 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) { | ||||
| 	tags = filteredTags | ||||
|  | ||||
| 	var proxyEndpointCreated *dynamicproxy.ProxyEndpoint | ||||
| 	if eptype == "host" { | ||||
| 	switch eptype { | ||||
| 	case "host": | ||||
| 		rootOrMatchingDomain, err := utils.PostPara(r, "rootname") | ||||
| 		if err != nil { | ||||
| 			utils.SendErrorResponse(w, "hostname not defined") | ||||
| @@ -415,7 +417,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) { | ||||
|  | ||||
| 		dynamicProxyRouter.AddProxyRouteToRuntime(preparedEndpoint) | ||||
| 		proxyEndpointCreated = &thisProxyEndpoint | ||||
| 	} else if eptype == "root" { | ||||
| 	case "root": | ||||
| 		//Get the default site options and target | ||||
| 		dsOptString, err := utils.PostPara(r, "defaultSiteOpt") | ||||
| 		if err != nil { | ||||
| @@ -469,7 +471,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) { | ||||
| 			return | ||||
| 		} | ||||
| 		proxyEndpointCreated = &rootRoutingEndpoint | ||||
| 	} else { | ||||
| 	default: | ||||
| 		//Invalid eptype | ||||
| 		utils.SendErrorResponse(w, "invalid endpoint type") | ||||
| 		return | ||||
| @@ -677,6 +679,70 @@ func ReverseProxyHandleAlias(w http.ResponseWriter, r *http.Request) { | ||||
| 	utils.SendOK(w) | ||||
| } | ||||
|  | ||||
| func ReverseProxyHandleSetTlsConfig(w http.ResponseWriter, r *http.Request) { | ||||
| 	if r.Method != http.MethodPost { | ||||
| 		utils.SendErrorResponse(w, "Method not supported") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	rootnameOrMatchingDomain, err := utils.PostPara(r, "ep") | ||||
| 	if err != nil { | ||||
| 		utils.SendErrorResponse(w, "Invalid ep given") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	tlsConfig, err := utils.PostPara(r, "tlsConfig") | ||||
| 	if err != nil { | ||||
| 		utils.SendErrorResponse(w, "Invalid TLS config given") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	tlsConfig = strings.TrimSpace(tlsConfig) | ||||
| 	if tlsConfig == "" { | ||||
| 		utils.SendErrorResponse(w, "TLS config cannot be empty") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	newTlsConfig := &tlscert.HostSpecificTlsBehavior{} | ||||
| 	err = json.Unmarshal([]byte(tlsConfig), newTlsConfig) | ||||
| 	if err != nil { | ||||
| 		utils.SendErrorResponse(w, "Invalid TLS config given: "+err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	//Load the target endpoint | ||||
| 	ept, err := dynamicProxyRouter.LoadProxy(rootnameOrMatchingDomain) | ||||
| 	if err != nil { | ||||
| 		utils.SendErrorResponse(w, err.Error()) | ||||
| 		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 | ||||
| 	readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(ept) | ||||
| 	if err != nil { | ||||
| 		utils.SendErrorResponse(w, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule) | ||||
|  | ||||
| 	//Save it to file | ||||
| 	err = SaveReverseProxyConfig(ept) | ||||
| 	if err != nil { | ||||
| 		utils.SendErrorResponse(w, "Failed to save TLS config: "+err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	utils.SendOK(w) | ||||
| } | ||||
|  | ||||
| func ReverseProxyHandleSetHostname(w http.ResponseWriter, r *http.Request) { | ||||
| 	if r.Method != http.MethodPost { | ||||
| 		utils.SendErrorResponse(w, "Method not supported") | ||||
| @@ -1015,6 +1081,7 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) | ||||
| func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) { | ||||
| 	js, err := json.Marshal(dynamicProxyRouter) | ||||
| 	if err != nil { | ||||
| 		SystemWideLogger.PrintAndLog("proxy-config", "Unable to marshal status data", err) | ||||
| 		utils.SendErrorResponse(w, "Unable to marshal status data") | ||||
| 		return | ||||
| 	} | ||||
|   | ||||
| @@ -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 */ | ||||
|   | ||||
| @@ -338,10 +338,45 @@ | ||||
|                     <!-- TLS / SSL --> | ||||
|                     <div class="rpconfig_content" rpcfg="ssl"> | ||||
|                         <div class="ui segment"> | ||||
|                             <p>Work In Progress <br> | ||||
|                             Please use the outer-most menu TLS / SSL tab for now. </p> | ||||
|                             <p>The table below shows which certificate will be served by Zoraxy when a client request the following hostnames.</p> | ||||
|                             <div class="ui blue message sni_grey_out_info" style="margin-bottom: 1em; display:none;"> | ||||
|                                 <i class="info circle icon"></i> | ||||
|                                 Certificate dropdowns are greyed out because SNI is enabled | ||||
|                             </div> | ||||
|                             <table class="ui celled small compact table sortable Tls_resolve_list"> | ||||
|                                 <thead> | ||||
|                                     <tr> | ||||
|                                         <th>Hostname</th> | ||||
|                                         <th class="no-sort">Resolve to Certificate</th> | ||||
|                                     </tr> | ||||
|                                 </thead> | ||||
|                                 <tbody> | ||||
|                                     <!-- Rows will be dynamically populated --> | ||||
|                                 </tbody> | ||||
|                             </table> | ||||
|                             <div class="ui checkbox" style="margin-top: 0.4em;"> | ||||
|                                 <input type="checkbox" class="Tls_EnableSNI"> | ||||
|                                 <label>Enable SNI<br> | ||||
|                                     <small>Resolve Server Name Indication (SNI) and automatically select a certificate</small> | ||||
|                                 </label> | ||||
|                             </div> | ||||
|                             <div class="ui checkbox" style="margin-top: 0.4em;"> | ||||
|                                 <input type="checkbox" class="Tls_EnableLegacyCertificateMatching"> | ||||
|                                 <label>Enable Legacy Certificate Matching<br> | ||||
|                                     <small>Use filename for hostname matching, faster but less accurate</small> | ||||
|                                 </label> | ||||
|                             </div> | ||||
|                             <div class="ui disabled checkbox" style="margin-top: 0.4em;"> | ||||
|                                 <input type="checkbox" class="Tls_EnableAutoHTTPS"> | ||||
|                                 <label>Enable Auto HTTPS (WIP)<br> | ||||
|                                     <small>Automatically request a certificate for the domain</small> | ||||
|                                 </label> | ||||
|                             </div> | ||||
|                              | ||||
|                             <br> | ||||
|                             <div class="ui divider"></div> | ||||
|                             <button class="ui basic small button getCertificateBtn" style="margin-left: 0.4em; margin-top: 0.4em;"><i class="green lock icon"></i> Get Certificate</button> | ||||
|                             <button class="ui basic small button getSelfSignCertBtn" style="margin-left: 0.4em; margin-top: 0.4em;"><i class="yellow lock icon"></i> Generate Self-Signed Certificate</button> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <!-- Custom Headers --> | ||||
| @@ -551,6 +586,28 @@ | ||||
|                         aliasDomains += `</small><br>`; | ||||
|                     } | ||||
|  | ||||
|                     //Build the sorting value | ||||
|                     let destSortValue = subd.ActiveOrigins.map(o => { | ||||
|                         // Check if it's an IP address (with optional port) | ||||
|                         let upstreamAddr = o.OriginIpOrDomain; | ||||
|                         let subpath = ""; | ||||
|                         if (upstreamAddr.indexOf("/") !== -1) { | ||||
|                             let parts = upstreamAddr.split("/"); | ||||
|                             subpath = parts.slice(1).join("/"); | ||||
|                             upstreamAddr = parts[0]; | ||||
|                         } | ||||
|                         let ipPortRegex = /^(\d{1,3}\.){3}\d{1,3}(:\d+)?$/; | ||||
|                         if (ipPortRegex.test(upstreamAddr)) { | ||||
|                             let [ip, port] = upstreamAddr.split(":"); | ||||
|                             // Convert IP to hex | ||||
|                             let hexIp = ip.split('.').map(x => ('00' + parseInt(x).toString(16)).slice(-2)).join(''); | ||||
|                             let hexPort = port ? (port.length < 5 ? port.padStart(5, '0') : port) : ''; | ||||
|                             return hexIp + (hexPort ? ':' + hexPort : '') + "/" + subpath; | ||||
|                         } | ||||
|                         // Otherwise, treat it as a domain name | ||||
|                         return upstreamAddr; | ||||
|                     }).join(","); | ||||
|                      | ||||
|                     //Build tag list | ||||
|                     let tagList = renderTagList(subd); | ||||
|                     let tagListEmpty = (subd.Tags.length == 0); | ||||
| @@ -567,7 +624,7 @@ | ||||
|                             ${aliasDomains} | ||||
|                             <small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small> | ||||
|                         </td> | ||||
|                         <td data-label="" editable="true" datatype="domain"> | ||||
|                         <td data-label="" editable="true" datatype="domain" data-sort-value="${destSortValue}" style="word-break: break-all;"> | ||||
|                             <div class="upstreamList"> | ||||
|                                 ${upstreams}         | ||||
|                             </div> | ||||
| @@ -711,6 +768,112 @@ | ||||
|         $("#httpProxyList").find(".editBtn").removeClass("disabled"); | ||||
|     } | ||||
|  | ||||
|     function saveTlsConfigs(uuid){ | ||||
|         let enableSNI = $("#httprpEditModal .Tls_EnableSNI")[0].checked; | ||||
|         let enableLegacyCertificateMatching = $("#httprpEditModal .Tls_EnableLegacyCertificateMatching")[0].checked; | ||||
|         let enableAutoHTTPS = $("#httprpEditModal .Tls_EnableAutoHTTPS")[0].checked; | ||||
|         let newTlsOption = { | ||||
|             "DisableSNI": !enableSNI, | ||||
|             "DisableLegacyCertificateMatching": !enableLegacyCertificateMatching, | ||||
|             "EnableAutoHTTPS": enableAutoHTTPS, | ||||
|         } | ||||
|         $.cjax({ | ||||
|             url: "/api/proxy/setTlsConfig", | ||||
|             method: "POST", | ||||
|             data: { | ||||
|                 "ep": uuid, | ||||
|                 "tlsConfig": JSON.stringify(newTlsOption) | ||||
|             }, | ||||
|             success: function(data){ | ||||
|                 if (data.error !== undefined){ | ||||
|                     msgbox(data.error, false, 3000); | ||||
|                 }else{ | ||||
|                     msgbox("TLS Config updated"); | ||||
|                 } | ||||
|                 updateTlsResolveList(uuid); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     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, | ||||
|             method: "GET", | ||||
|             success: function(data) { | ||||
|                 // Populate the TLS resolve list | ||||
|                 let resolveList = editor.find(".Tls_resolve_list tbody"); | ||||
|                 resolveList.empty(); // Clear existing entries | ||||
|                 let primaryDomain = data.domain; | ||||
|                 let aliasDomains = data.alias_domains || []; | ||||
|                 let certMap = data.domain_key_pair; | ||||
|  | ||||
|                 // Add primary domain entry | ||||
|                 resolveList.append(` | ||||
|                     <tr> | ||||
|                         <td>${primaryDomain}</td> | ||||
|                         <td class="certificateDropdown" domain="${primaryDomain}">${certMap[primaryDomain] || "Fallback Certificate"}</td> | ||||
|                     </tr> | ||||
|                 `); | ||||
|                 aliasDomains.forEach(alias => { | ||||
|                     resolveList.append(` | ||||
|                         <tr> | ||||
|                             <td>${alias}</td> | ||||
|                             <td class="certificateDropdown" domain="${alias}">${certMap[alias] || "Fallback Certificate"}</td> | ||||
|                         </tr> | ||||
|                     `); | ||||
|                 }); | ||||
|  | ||||
|                 //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(); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function saveProxyInlineEdit(uuid){ | ||||
|         let editor = $("#httprpEditModal"); | ||||
|          | ||||
| @@ -857,6 +1020,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"){ | ||||
| @@ -985,6 +1171,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 = $('<div class="ui fluid selection dropdown"></div>'); | ||||
|                 let menu = $('<div class="menu"></div>'); | ||||
|                 data.forEach(cert => { | ||||
|                     menu.append(`<div class="item" data-value="${cert}">${cert}</div>`); | ||||
|                 }); | ||||
|                 // Add a hidden input to store the selected certificate | ||||
|                 dropdown.append('<input type="hidden" name="certificate">'); | ||||
|                 dropdown.append('<i class="dropdown icon"></i>'); | ||||
|                 dropdown.append('<div class="default text">Fallback Certificate</div>'); | ||||
|                 dropdown.append(menu); | ||||
|                 callback(dropdown); | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     //Initialize the http proxy rule editor | ||||
|     function initHttpProxyRuleEditorModal(rulepayload){ | ||||
|         let subd = JSON.parse(JSON.stringify(rulepayload)); | ||||
| @@ -1086,39 +1294,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)); | ||||
| @@ -1148,6 +1323,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 ------------ */  | ||||
| @@ -1245,6 +1422,60 @@ | ||||
|         editor.find(".RateLimit").off("change").on("change", rateLimitChangeEvent); | ||||
|  | ||||
|         /* ------------ 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); | ||||
|  | ||||
|         editor.find(".Tls_EnableSNI").off("change").on("change", function() { | ||||
|             saveTlsConfigs(uuid); | ||||
|         }); | ||||
|         editor.find(".Tls_EnableLegacyCertificateMatching").off("change").on("change", function() { | ||||
|             saveTlsConfigs(uuid); | ||||
|         }); | ||||
|         editor.find(".Tls_EnableAutoHTTPS").off("change").on("change", function() { | ||||
|             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 ------------ */ | ||||
|         (()=>{ | ||||
| @@ -1308,7 +1539,6 @@ | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /* | ||||
|         Page Initialization Functions  | ||||
|     */ | ||||
| @@ -1333,7 +1563,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 | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -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); | ||||
|   | ||||
| @@ -16,6 +16,7 @@ | ||||
|                         <th>Target Address</th> | ||||
|                         <th>Mode</th> | ||||
|                         <th>Timeout (s)</th> | ||||
|                         <th>Enable Logging</th> | ||||
|                         <th>Actions</th> | ||||
|                     </tr> | ||||
|                 </thead> | ||||
| @@ -73,6 +74,22 @@ | ||||
|                     <small>Forward UDP request on this listening socket</small></label> | ||||
|                 </div> | ||||
|             </div> | ||||
|            <div class="field"> | ||||
|                 <div class="ui toggle checkbox"> | ||||
|                     <input type="checkbox" tabindex="0" name="useProxyProtocol" class="hidden"> | ||||
|                     <label>Enable Proxy Protocol V1<br> | ||||
|                         <small>Enable TCP Proxy Protocol header V1</small> | ||||
|                     </label> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="field"> | ||||
|                 <div class="ui toggle checkbox"> | ||||
|                     <input type="checkbox" tabindex="0" name="enableLogging" class="hidden"> | ||||
|                     <label>Enable Logging<br> | ||||
|                         <small>Enable logging of connection status and errors for this rule</small> | ||||
|                     </label> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <button id="addStreamProxyButton" class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>   | ||||
|             <button id="editStreamProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event, this);" style="display:none;"><i class="ui green check icon"></i> Update</button>   | ||||
|             <button class="ui basic red button" onclick="event.preventDefault(); cancelStreamProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>   | ||||
| @@ -120,7 +137,7 @@ | ||||
|         }); | ||||
|              | ||||
|         function clearStreamProxyAddEditForm(){ | ||||
|             $('#streamProxyForm input, #streamProxyForm select').val(''); | ||||
|             $('#streamProxyForm').find('input:not([type=checkbox]), select').val(''); | ||||
|             $('#streamProxyForm select').dropdown('clear'); | ||||
|             $("#streamProxyForm input[name=timeout]").val(10); | ||||
|             $("#streamProxyForm .toggle.checkbox").checkbox("set unchecked"); | ||||
| @@ -195,6 +212,10 @@ | ||||
|                         modeText.push("UDP") | ||||
|                     } | ||||
|  | ||||
|                     if (config.UseProxyProtocol){ | ||||
|                         modeText.push("ProxyProtocol V1") | ||||
|                     } | ||||
|  | ||||
|                     modeText = modeText.join(" & ") | ||||
|  | ||||
|                     var thisConfig = encodeURIComponent(JSON.stringify(config)); | ||||
| @@ -207,6 +228,10 @@ | ||||
|                     row.append($('<td>').text(config.ProxyTargetAddr)); | ||||
|                     row.append($('<td>').text(modeText)); | ||||
|                     row.append($('<td>').text(config.Timeout)); | ||||
|                     row.append($('<td>').html(config.EnableLogging ?  | ||||
|                         '<i class="green check icon" title="Logging Enabled"></i>' :  | ||||
|                         '<i class="red times icon" title="Logging Disabled"></i>' | ||||
|                     )); | ||||
|                     row.append($('<td>').html(` | ||||
|                         ${startButton} | ||||
|                         <button onclick="editTCPProxyConfig('${config.UUID}');" class="ui circular basic mini icon button" title="Edit Config"><i class="edit icon"></i></button> | ||||
| @@ -252,6 +277,22 @@ | ||||
|                             $(checkboxEle).checkbox("set unchecked"); | ||||
|                         } | ||||
|                         return; | ||||
|                     }else if (key == "UseProxyProtocol"){ | ||||
|                         let checkboxEle = $("#streamProxyForm input[name=useProxyProtocol]").parent(); | ||||
|                         if (value === true){ | ||||
|                             $(checkboxEle).checkbox("set checked"); | ||||
|                         }else{ | ||||
|                             $(checkboxEle).checkbox("set unchecked"); | ||||
|                         } | ||||
|                         return; | ||||
|                     }else if (key == "EnableLogging"){ | ||||
|                         let checkboxEle = $("#streamProxyForm input[name=enableLogging]").parent(); | ||||
|                         if (value === true){ | ||||
|                             $(checkboxEle).checkbox("set checked"); | ||||
|                         }else{ | ||||
|                             $(checkboxEle).checkbox("set unchecked"); | ||||
|                         } | ||||
|                         return; | ||||
|                     }else if (key == "ListeningAddress"){ | ||||
|                         field = $("#streamProxyForm input[name=listenAddr]"); | ||||
|                     }else if (key == "ProxyTargetAddr"){ | ||||
| @@ -301,6 +342,8 @@ | ||||
|                     proxyAddr: $("#streamProxyForm input[name=proxyAddr]").val().trim(), | ||||
|                     useTCP: $("#streamProxyForm input[name=useTCP]")[0].checked , | ||||
|                     useUDP: $("#streamProxyForm input[name=useUDP]")[0].checked , | ||||
|                     useProxyProtocol: $("#streamProxyForm input[name=useProxyProtocol]")[0].checked , | ||||
|                     enableLogging: $("#streamProxyForm input[name=enableLogging]")[0].checked , | ||||
|                     timeout: parseInt($("#streamProxyForm input[name=timeout]").val().trim()), | ||||
|                 }, | ||||
|                 success: function(response) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Toby Chui
					Toby Chui