mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-08-05 20:58:28 +02:00
Restructure TLS options
- Moved certification related functions into tlscert module - Added specific host TLS behavior logic - Added support for disabling SNI and manually overwrite preferred certificate to serve - Fixed SSO requestHeaders null bug
This commit is contained in:
17
src/api.go
17
src/api.go
@@ -72,15 +72,20 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
|
||||
|
||||
// Register the APIs for TLS / SSL certificate management functions
|
||||
func RegisterTLSAPIs(authRouter *auth.RouterDef) {
|
||||
//Global certificate settings
|
||||
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
|
||||
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
|
||||
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
|
||||
authRouter.HandleFunc("/api/cert/download", handleCertDownload)
|
||||
authRouter.HandleFunc("/api/cert/list", handleListCertificate)
|
||||
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains)
|
||||
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
|
||||
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
|
||||
authRouter.HandleFunc("/api/cert/resolve", handleCertTryResolve)
|
||||
authRouter.HandleFunc("/api/cert/setPreferredCertificate", handleSetDomainPreferredCertificate)
|
||||
|
||||
//Certificate store functions
|
||||
authRouter.HandleFunc("/api/cert/upload", tlsCertManager.HandleCertUpload)
|
||||
authRouter.HandleFunc("/api/cert/download", tlsCertManager.HandleCertDownload)
|
||||
authRouter.HandleFunc("/api/cert/list", tlsCertManager.HandleListCertificate)
|
||||
authRouter.HandleFunc("/api/cert/listdomains", tlsCertManager.HandleListDomains)
|
||||
authRouter.HandleFunc("/api/cert/checkDefault", tlsCertManager.HandleDefaultCertCheck)
|
||||
authRouter.HandleFunc("/api/cert/delete", tlsCertManager.HandleCertRemove)
|
||||
authRouter.HandleFunc("/api/cert/selfsign", tlsCertManager.HandleSelfSignCertGenerate)
|
||||
}
|
||||
|
||||
// Register the APIs for Authentication handlers like Forward Auth and OAUTH2
|
||||
|
336
src/cert.go
336
src/cert.go
@@ -1,188 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
// Check if the default certificates is correctly setup
|
||||
func handleDefaultCertCheck(w http.ResponseWriter, r *http.Request) {
|
||||
type CheckResult struct {
|
||||
DefaultPubExists bool
|
||||
DefaultPriExists bool
|
||||
}
|
||||
|
||||
pub, pri := tlsCertManager.DefaultCertExistsSep()
|
||||
js, _ := json.Marshal(CheckResult{
|
||||
pub,
|
||||
pri,
|
||||
})
|
||||
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// Return a list of domains where the certificates covers
|
||||
func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
filenames, err := tlsCertManager.ListCertDomains()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
showDate, _ := utils.GetPara(r, "date")
|
||||
if showDate == "true" {
|
||||
type CertInfo struct {
|
||||
Domain string
|
||||
LastModifiedDate string
|
||||
ExpireDate string
|
||||
RemainingDays int
|
||||
UseDNS bool
|
||||
}
|
||||
|
||||
results := []*CertInfo{}
|
||||
|
||||
for _, filename := range filenames {
|
||||
certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".pem")
|
||||
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
|
||||
fileInfo, err := os.Stat(certFilepath)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid domain certificate discovered: "+filename)
|
||||
return
|
||||
}
|
||||
modifiedTime := fileInfo.ModTime().Format("2006-01-02 15:04:05")
|
||||
|
||||
certExpireTime := "Unknown"
|
||||
certBtyes, err := os.ReadFile(certFilepath)
|
||||
expiredIn := 0
|
||||
if err != nil {
|
||||
//Unable to load this file
|
||||
continue
|
||||
} else {
|
||||
//Cert loaded. Check its expire time
|
||||
block, _ := pem.Decode(certBtyes)
|
||||
if block != nil {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err == nil {
|
||||
certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05")
|
||||
|
||||
duration := cert.NotAfter.Sub(time.Now())
|
||||
|
||||
// Convert the duration to days
|
||||
expiredIn = int(duration.Hours() / 24)
|
||||
}
|
||||
}
|
||||
}
|
||||
certInfoFilename := filepath.Join(tlsCertManager.CertStore, filename+".json")
|
||||
useDNSValidation := false //Default to false for HTTP TLS certificates
|
||||
certInfo, err := acme.LoadCertInfoJSON(certInfoFilename) //Note: Not all certs have info json
|
||||
if err == nil {
|
||||
useDNSValidation = certInfo.UseDNS
|
||||
}
|
||||
|
||||
thisCertInfo := CertInfo{
|
||||
Domain: filename,
|
||||
LastModifiedDate: modifiedTime,
|
||||
ExpireDate: certExpireTime,
|
||||
RemainingDays: expiredIn,
|
||||
UseDNS: useDNSValidation,
|
||||
}
|
||||
|
||||
results = append(results, &thisCertInfo)
|
||||
}
|
||||
|
||||
// convert ExpireDate to date object and sort asc
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
date1, _ := time.Parse("2006-01-02 15:04:05", results[i].ExpireDate)
|
||||
date2, _ := time.Parse("2006-01-02 15:04:05", results[j].ExpireDate)
|
||||
return date1.Before(date2)
|
||||
})
|
||||
|
||||
js, _ := json.Marshal(results)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(js)
|
||||
} else {
|
||||
response, err := json.Marshal(filenames)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(response)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// List all certificates and map all their domains to the cert filename
|
||||
func handleListDomains(w http.ResponseWriter, r *http.Request) {
|
||||
filenames, err := os.ReadDir("./conf/certs/")
|
||||
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
certnameToDomainMap := map[string]string{}
|
||||
for _, filename := range filenames {
|
||||
if filename.IsDir() {
|
||||
continue
|
||||
}
|
||||
certFilepath := filepath.Join("./conf/certs/", filename.Name())
|
||||
|
||||
certBtyes, err := os.ReadFile(certFilepath)
|
||||
if err != nil {
|
||||
// Unable to load this file
|
||||
SystemWideLogger.PrintAndLog("TLS", "Unable to load certificate: "+certFilepath, err)
|
||||
continue
|
||||
} else {
|
||||
// Cert loaded. Check its expiry time
|
||||
block, _ := pem.Decode(certBtyes)
|
||||
if block != nil {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err == nil {
|
||||
certname := strings.TrimSuffix(filepath.Base(certFilepath), filepath.Ext(certFilepath))
|
||||
for _, dnsName := range cert.DNSNames {
|
||||
certnameToDomainMap[dnsName] = certname
|
||||
}
|
||||
certnameToDomainMap[cert.Subject.CommonName] = certname
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requireCompact, _ := utils.GetPara(r, "compact")
|
||||
if requireCompact == "true" {
|
||||
result := make(map[string][]string)
|
||||
|
||||
for key, value := range certnameToDomainMap {
|
||||
if _, ok := result[value]; !ok {
|
||||
result[value] = make([]string, 0)
|
||||
}
|
||||
|
||||
result[value] = append(result[value], key)
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(result)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(certnameToDomainMap)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// Handle front-end toggling TLS mode
|
||||
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
||||
currentTlsSetting := true //Default to true
|
||||
@@ -193,11 +19,12 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
||||
sysdb.Read("settings", "usetls", ¤tTlsSetting)
|
||||
}
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
//Get the current status
|
||||
js, _ := json.Marshal(currentTlsSetting)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else if r.Method == http.MethodPost {
|
||||
case http.MethodPost:
|
||||
newState, err := utils.PostBool(r, "set")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "new state not set or invalid")
|
||||
@@ -213,7 +40,7 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
||||
dynamicProxyRouter.UpdateTLSSetting(false)
|
||||
}
|
||||
utils.SendOK(w)
|
||||
} else {
|
||||
default:
|
||||
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
@@ -231,135 +58,21 @@ func handleSetTlsRequireLatest(w http.ResponseWriter, r *http.Request) {
|
||||
js, _ := json.Marshal(reqLatestTLS)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if newState == "true" {
|
||||
switch newState {
|
||||
case "true":
|
||||
sysdb.Write("settings", "forceLatestTLS", true)
|
||||
SystemWideLogger.Println("Updating minimum TLS version to v1.2 or above")
|
||||
dynamicProxyRouter.UpdateTLSVersion(true)
|
||||
} else if newState == "false" {
|
||||
case "false":
|
||||
sysdb.Write("settings", "forceLatestTLS", false)
|
||||
SystemWideLogger.Println("Updating minimum TLS version to v1.0 or above")
|
||||
dynamicProxyRouter.UpdateTLSVersion(false)
|
||||
} else {
|
||||
default:
|
||||
utils.SendErrorResponse(w, "invalid state given")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle download of the selected certificate
|
||||
func handleCertDownload(w http.ResponseWriter, r *http.Request) {
|
||||
// get the certificate name
|
||||
certname, err := utils.GetPara(r, "certname")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid certname given")
|
||||
return
|
||||
}
|
||||
certname = filepath.Base(certname) //prevent path escape
|
||||
|
||||
// check if the cert exists
|
||||
pubKey := filepath.Join(filepath.Join("./conf/certs"), certname+".key")
|
||||
priKey := filepath.Join(filepath.Join("./conf/certs"), certname+".pem")
|
||||
|
||||
if utils.FileExists(pubKey) && utils.FileExists(priKey) {
|
||||
//Zip them and serve them via http download
|
||||
seeking, _ := utils.GetBool(r, "seek")
|
||||
if seeking {
|
||||
//This request only check if the key exists. Do not provide download
|
||||
utils.SendOK(w)
|
||||
return
|
||||
}
|
||||
|
||||
//Serve both file in zip
|
||||
zipTmpFolder := "./tmp/download"
|
||||
os.MkdirAll(zipTmpFolder, 0775)
|
||||
zipFileName := filepath.Join(zipTmpFolder, certname+".zip")
|
||||
err := utils.ZipFiles(zipFileName, pubKey, priKey)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create zip file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer os.Remove(zipFileName) // Clean up the zip file after serving
|
||||
|
||||
// Serve the zip file
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\""+certname+"_export.zip\"")
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
http.ServeFile(w, r, zipFileName)
|
||||
} else {
|
||||
//Not both key exists
|
||||
utils.SendErrorResponse(w, "invalid key-pairs: private key or public key not found in key store")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Handle upload of the certificate
|
||||
func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
||||
// check if request method is POST
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// get the key type
|
||||
keytype, err := utils.GetPara(r, "ktype")
|
||||
overWriteFilename := ""
|
||||
if err != nil {
|
||||
http.Error(w, "Not defined key type (pub / pri)", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// get the domain
|
||||
domain, err := utils.GetPara(r, "domain")
|
||||
if err != nil {
|
||||
//Assume localhost
|
||||
domain = "default"
|
||||
}
|
||||
|
||||
if keytype == "pub" {
|
||||
overWriteFilename = domain + ".pem"
|
||||
} else if keytype == "pri" {
|
||||
overWriteFilename = domain + ".key"
|
||||
} else {
|
||||
http.Error(w, "Not supported keytype: "+keytype, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// parse multipart form data
|
||||
err = r.ParseMultipartForm(10 << 20) // 10 MB
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// get file from form data
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// create file in upload directory
|
||||
os.MkdirAll("./conf/certs", 0775)
|
||||
f, err := os.Create(filepath.Join("./conf/certs", overWriteFilename))
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// copy file contents to destination file
|
||||
_, err = io.Copy(f, file)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
//Update cert list
|
||||
tlsCertManager.UpdateLoadedCertList()
|
||||
|
||||
// send response
|
||||
fmt.Fprintln(w, "File upload successful!")
|
||||
}
|
||||
|
||||
func handleCertTryResolve(w http.ResponseWriter, r *http.Request) {
|
||||
// get the domain
|
||||
domain, err := utils.GetPara(r, "domain")
|
||||
@@ -441,15 +154,40 @@ func handleCertTryResolve(w http.ResponseWriter, r *http.Request) {
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// Handle cert remove
|
||||
func handleCertRemove(w http.ResponseWriter, r *http.Request) {
|
||||
func handleSetDomainPreferredCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
//Get the domain
|
||||
domain, err := utils.PostPara(r, "domain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid domain given")
|
||||
return
|
||||
}
|
||||
err = tlsCertManager.RemoveCert(domain)
|
||||
|
||||
//Get the certificate name
|
||||
certName, err := utils.PostPara(r, "certname")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
utils.SendErrorResponse(w, "invalid certificate name given")
|
||||
return
|
||||
}
|
||||
|
||||
//Load the target endpoint
|
||||
ept, err := dynamicProxyRouter.GetProxyEndpointById(domain, true)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "proxy rule not found for domain: "+domain)
|
||||
return
|
||||
}
|
||||
|
||||
//Set the preferred certificate for the domain
|
||||
err = dynamicProxyRouter.SetPreferredCertificateForDomain(ept, domain, certName)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to set preferred certificate: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = SaveReverseProxyConfig(ept)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to save reverse proxy config: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
|
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
|
||||
}
|
||||
|
@@ -21,10 +21,10 @@ type CertCache struct {
|
||||
}
|
||||
|
||||
type HostSpecificTlsBehavior struct {
|
||||
DisableSNI bool //If SNI is enabled for this server name
|
||||
DisableLegacyCertificateMatching bool //If legacy certificate matching is disabled for this server name
|
||||
EnableAutoHTTPS bool //If auto HTTPS is enabled for this server name
|
||||
PreferredCertificate string //Preferred certificate for this server name, if empty, use the first matching certificate
|
||||
DisableSNI bool //If SNI is enabled for this server name
|
||||
DisableLegacyCertificateMatching bool //If legacy certificate matching is disabled for this server name
|
||||
EnableAutoHTTPS bool //If auto HTTPS is enabled for this server name
|
||||
PreferredCertificate map[string]string //Preferred certificate for this server name, if empty, use the first matching certificate
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
@@ -34,13 +34,12 @@ type Manager struct {
|
||||
|
||||
/* External handlers */
|
||||
hostSpecificTlsBehavior func(serverName string) (*HostSpecificTlsBehavior, error) // Function to get host specific TLS behavior, if nil, use global TLS options
|
||||
verbal bool
|
||||
}
|
||||
|
||||
//go:embed localhost.pem localhost.key
|
||||
var buildinCertStore embed.FS
|
||||
|
||||
func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager, error) {
|
||||
func NewManager(certStore string, logger *logger.Logger) (*Manager, error) {
|
||||
if !utils.FileExists(certStore) {
|
||||
os.MkdirAll(certStore, 0775)
|
||||
}
|
||||
@@ -63,7 +62,6 @@ func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager,
|
||||
CertStore: certStore,
|
||||
LoadedCerts: []*CertCache{},
|
||||
hostSpecificTlsBehavior: defaultHostSpecificTlsBehavior, //Default to no SNI and no auto HTTPS
|
||||
verbal: verbal,
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
@@ -82,7 +80,7 @@ func GetDefaultHostSpecificTlsBehavior() *HostSpecificTlsBehavior {
|
||||
DisableSNI: false,
|
||||
DisableLegacyCertificateMatching: false,
|
||||
EnableAutoHTTPS: false,
|
||||
PreferredCertificate: "",
|
||||
PreferredCertificate: map[string]string{}, // No preferred certificate, use the first matching certificate
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +88,10 @@ func defaultHostSpecificTlsBehavior(serverName string) (*HostSpecificTlsBehavior
|
||||
return GetDefaultHostSpecificTlsBehavior(), nil
|
||||
}
|
||||
|
||||
func (m *Manager) SetHostSpecificTlsBehavior(fn func(serverName string) (*HostSpecificTlsBehavior, error)) {
|
||||
m.hostSpecificTlsBehavior = fn
|
||||
}
|
||||
|
||||
// Update domain mapping from file
|
||||
func (m *Manager) UpdateLoadedCertList() error {
|
||||
//Get a list of certificates from file
|
||||
@@ -213,13 +215,17 @@ func (m *Manager) GetCertificateByHostname(hostname string) (string, string, err
|
||||
if err != nil {
|
||||
tlsBehavior, _ = defaultHostSpecificTlsBehavior(hostname)
|
||||
}
|
||||
preferredCertificate, ok := tlsBehavior.PreferredCertificate[hostname]
|
||||
if !ok {
|
||||
preferredCertificate = ""
|
||||
}
|
||||
|
||||
if tlsBehavior.DisableSNI && tlsBehavior.PreferredCertificate != "" &&
|
||||
utils.FileExists(filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".pem")) &&
|
||||
utils.FileExists(filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".key")) {
|
||||
if tlsBehavior.DisableSNI && preferredCertificate != "" &&
|
||||
utils.FileExists(filepath.Join(m.CertStore, preferredCertificate+".pem")) &&
|
||||
utils.FileExists(filepath.Join(m.CertStore, preferredCertificate+".key")) {
|
||||
//User setup a Preferred certificate, use the preferred certificate directly
|
||||
pubKey = filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".pem")
|
||||
priKey = filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".key")
|
||||
pubKey = filepath.Join(m.CertStore, preferredCertificate+".pem")
|
||||
priKey = filepath.Join(m.CertStore, preferredCertificate+".key")
|
||||
} else {
|
||||
if !tlsBehavior.DisableLegacyCertificateMatching &&
|
||||
utils.FileExists(filepath.Join(m.CertStore, hostname+".pem")) &&
|
||||
|
@@ -140,7 +140,7 @@ func ReverseProxtInit() {
|
||||
err := LoadReverseProxyConfig(conf)
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("proxy-config", "Failed to load config file: "+filepath.Base(conf), err)
|
||||
return
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -717,6 +717,11 @@ func ReverseProxyHandleSetTlsConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if newTlsConfig.PreferredCertificate == nil {
|
||||
//No update needed, reuse the current TLS config
|
||||
newTlsConfig.PreferredCertificate = ept.TlsOptions.PreferredCertificate
|
||||
}
|
||||
|
||||
ept.TlsOptions = newTlsConfig
|
||||
|
||||
//Prepare to replace the current routing rule
|
||||
|
@@ -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 */
|
||||
|
@@ -339,11 +339,15 @@
|
||||
<div class="rpconfig_content" rpcfg="ssl">
|
||||
<div class="ui segment">
|
||||
<p>The table below shows which certificate will be served by Zoraxy when a client request the following hostnames.</p>
|
||||
<table class="ui celled small compact table Tls_resolve_list">
|
||||
<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>Resolve to Certificate</th>
|
||||
<th class="no-sort">Resolve to Certificate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -359,18 +363,20 @@
|
||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||
<input type="checkbox" class="Tls_EnableLegacyCertificateMatching">
|
||||
<label>Enable Legacy Certificate Matching<br>
|
||||
<small>Use legacy filename / hostname matching for loading certificates</small>
|
||||
<small>Use filename for hostname matching, faster but less accurate</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||
<div class="ui disabled checkbox" style="margin-top: 0.4em;">
|
||||
<input type="checkbox" class="Tls_EnableAutoHTTPS">
|
||||
<label>Enable Auto HTTPS<br>
|
||||
<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 -->
|
||||
@@ -747,7 +753,7 @@
|
||||
let newTlsOption = {
|
||||
"DisableSNI": !enableSNI,
|
||||
"DisableLegacyCertificateMatching": !enableLegacyCertificateMatching,
|
||||
"EnableAutoHTTPS": enableAutoHTTPS
|
||||
"EnableAutoHTTPS": enableAutoHTTPS,
|
||||
}
|
||||
$.cjax({
|
||||
url: "/api/proxy/setTlsConfig",
|
||||
@@ -769,6 +775,9 @@
|
||||
|
||||
function updateTlsResolveList(uuid){
|
||||
let editor = $("#httprpEditModalWrapper");
|
||||
editor.find(".certificateDropdown .ui.dropdown").off("change");
|
||||
editor.find(".certificateDropdown .ui.dropdown").remove();
|
||||
|
||||
//Update the TLS resolve list
|
||||
$.ajax({
|
||||
url: "/api/cert/resolve?domain=" + uuid,
|
||||
@@ -785,17 +794,60 @@
|
||||
resolveList.append(`
|
||||
<tr>
|
||||
<td>${primaryDomain}</td>
|
||||
<td>${certMap[primaryDomain] || "Fallback Certificate"}</td>
|
||||
<td class="certificateDropdown" domain="${primaryDomain}">${certMap[primaryDomain] || "Fallback Certificate"}</td>
|
||||
</tr>
|
||||
`);
|
||||
aliasDomains.forEach(alias => {
|
||||
resolveList.append(`
|
||||
<tr>
|
||||
<td>${alias}</td>
|
||||
<td>${certMap[alias] || "Fallback Certificate"}</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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -946,6 +998,29 @@
|
||||
renewCertificate(renewDomainKey, false, btn);
|
||||
}
|
||||
|
||||
function generateSelfSignedCertificate(uuid, domains, btn=undefined){
|
||||
let payload = JSON.stringify(domains);
|
||||
$.cjax({
|
||||
url: "/api/cert/selfsign",
|
||||
data: {
|
||||
"cn": uuid,
|
||||
"domains": payload
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error == undefined){
|
||||
msgbox("Self-Signed Certificate Generated", true);
|
||||
resyncProxyEditorConfig();
|
||||
if (typeof(initManagedDomainCertificateList) != undefined){
|
||||
//Re-init the managed domain certificate list
|
||||
initManagedDomainCertificateList();
|
||||
}
|
||||
}else{
|
||||
msgbox(data.error, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Tags & Search */
|
||||
function handleSearchInput(event){
|
||||
if (event.key == "Escape"){
|
||||
@@ -1074,6 +1149,28 @@
|
||||
return subd;
|
||||
}
|
||||
|
||||
// Generate a certificate dropdown for the HTTP Proxy Rule Editor
|
||||
// so user can pick which certificate they want to use for the current editing hostname
|
||||
function generateCertificateDropdown(callback){
|
||||
$.ajax({
|
||||
url: "/api/cert/list",
|
||||
method: "GET",
|
||||
success: function(data) {
|
||||
let dropdown = $('<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));
|
||||
@@ -1175,39 +1272,6 @@
|
||||
|
||||
});
|
||||
editor.find(".downstream_alias_hostname").html(aliasHTML);
|
||||
|
||||
//TODO: Move this to SSL TLS section
|
||||
let enableQuickRequestButton = true;
|
||||
let domains = [subd.RootOrMatchingDomain]; //Domain for getting certificate if needed
|
||||
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
|
||||
let thisAliasName = subd.MatchingDomainAlias[i];
|
||||
domains.push(thisAliasName);
|
||||
}
|
||||
|
||||
//Check if the domain or alias contains wildcard, if yes, disabled the get certificate button
|
||||
if (subd.RootOrMatchingDomain.indexOf("*") > -1){
|
||||
enableQuickRequestButton = false;
|
||||
}
|
||||
|
||||
if (subd.MatchingDomainAlias != undefined){
|
||||
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
|
||||
if (subd.MatchingDomainAlias[i].indexOf("*") > -1){
|
||||
enableQuickRequestButton = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
|
||||
if (enableQuickRequestButton){
|
||||
editor.find(".getCertificateBtn").removeClass("disabled");
|
||||
}else{
|
||||
editor.find(".getCertificateBtn").addClass("disabled");
|
||||
}
|
||||
|
||||
editor.find(".getCertificateBtn").off("click").on("click", function(){
|
||||
requestCertificateForExistingHost(uuid, certificateDomains, this);
|
||||
});
|
||||
|
||||
/* ------------ Upstreams ------------ */
|
||||
editor.find(".upstream_list").html(renderUpstreamList(subd));
|
||||
@@ -1237,6 +1301,8 @@
|
||||
editor.find(".vdir_list").html(renderVirtualDirectoryList(subd));
|
||||
editor.find(".editVdirBtn").off("click").on("click", function(){
|
||||
quickEditVdir(uuid);
|
||||
//Temporary restore scroll
|
||||
$("body").css("overflow", "auto");
|
||||
});
|
||||
|
||||
/* ------------ Alias ------------ */
|
||||
@@ -1336,6 +1402,7 @@
|
||||
/* ------------ TLS ------------ */
|
||||
updateTlsResolveList(uuid);
|
||||
editor.find(".Tls_EnableSNI").prop("checked", !subd.TlsOptions.DisableSNI);
|
||||
|
||||
editor.find(".Tls_EnableLegacyCertificateMatching").prop("checked", !subd.TlsOptions.DisableLegacyCertificateMatching);
|
||||
editor.find(".Tls_EnableAutoHTTPS").prop("checked", !!subd.TlsOptions.EnableAutoHTTPS);
|
||||
|
||||
@@ -1349,6 +1416,45 @@
|
||||
saveTlsConfigs(uuid);
|
||||
});
|
||||
|
||||
/* Quick access to get certificate for the current host */
|
||||
let enableQuickRequestButton = true;
|
||||
let domains = [subd.RootOrMatchingDomain]; //Domain for getting certificate if needed
|
||||
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
|
||||
let thisAliasName = subd.MatchingDomainAlias[i];
|
||||
domains.push(thisAliasName);
|
||||
}
|
||||
|
||||
//Check if the domain or alias contains wildcard, if yes, disabled the get certificate button
|
||||
if (subd.RootOrMatchingDomain.indexOf("*") > -1){
|
||||
enableQuickRequestButton = false;
|
||||
}
|
||||
|
||||
if (subd.MatchingDomainAlias != undefined){
|
||||
for (var i = 0; i < subd.MatchingDomainAlias.length; i++){
|
||||
if (subd.MatchingDomainAlias[i].indexOf("*") > -1){
|
||||
enableQuickRequestButton = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (enableQuickRequestButton){
|
||||
editor.find(".getCertificateBtn").removeClass("disabled");
|
||||
}else{
|
||||
editor.find(".getCertificateBtn").addClass("disabled");
|
||||
}
|
||||
|
||||
editor.find(".getCertificateBtn").off("click").on("click", function(){
|
||||
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
|
||||
requestCertificateForExistingHost(uuid, certificateDomains, this);
|
||||
});
|
||||
|
||||
// Bind event to self-signed certificate button
|
||||
editor.find(".getSelfSignCertBtn").off("click").on("click", function() {
|
||||
generateSelfSignedCertificate(uuid, domains, this);
|
||||
});
|
||||
|
||||
|
||||
|
||||
/* ------------ Tags ------------ */
|
||||
(()=>{
|
||||
let payload = encodeURIComponent(JSON.stringify({
|
||||
@@ -1411,7 +1517,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Page Initialization Functions
|
||||
*/
|
||||
@@ -1436,7 +1541,9 @@
|
||||
// there is a chance where the user has modified the Vdir
|
||||
// we need to get the latest setting from server side and
|
||||
// render it again
|
||||
updateVdirInProxyEditor();
|
||||
resyncProxyEditorConfig();
|
||||
window.scrollTo(0, 0);
|
||||
$("body").css("overflow", "hidden");
|
||||
} else {
|
||||
listProxyEndpoints();
|
||||
//Reset the tag filter
|
||||
|
@@ -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);
|
||||
|
Reference in New Issue
Block a user