31 Commits

Author SHA1 Message Date
c4c10d2130 Fixed #713
- Fixed sorting destination not working bug
2025-07-12 19:52:59 +08:00
4d3d1b25cb 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
2025-07-12 19:30:55 +08:00
118b5e5114 Merge pull request #723 from 7brend7/fix-empty-sso-advanced-params
fix empty sso advanced parameters
2025-07-08 19:00:25 +08:00
ad53b894c0 Update src/mod/auth/sso/forward/forward.go
Co-authored-by: James Elliott <james-d-elliott@users.noreply.github.com>
2025-07-08 12:38:08 +08:00
45506c8772 Added cert resolve viewer
- Added certificate resolve viewer on HTTP proxy rule editor
- Exposed SNI options (wip)
- Code optimize
2025-07-07 14:18:10 +08:00
c091b9d1ca Added content security policy structure
- Added content security policy header generators structure (current not in used)
2025-07-07 13:25:07 +08:00
691cb603ce Merge pull request #724 from 7brend7/sort-list-of-certificates
sort list of loaded certificates by ExpireDate
2025-07-07 10:51:35 +08:00
e53724d6e5 sort list of loaded certificates by ExpireDate 2025-07-06 22:40:10 +03:00
e225407b03 fix empty sso advanced parameters 2025-07-06 22:25:17 +03:00
273cae2a98 Merge pull request #719 from jemmy1794/main
Add EnableLogging to Stream Proxy for log control
2025-07-03 20:25:39 +08:00
6b3b89f7bf Add EnableLogging to Stream Proxy for log control
- Add `EnableLogging` to control TCP/UDP Connection logs to reduce log latency.
- Add `Enable Logging` Option in Stream Proxy rule.
- Update Stream Proxy UI.
2025-07-03 09:01:46 +08:00
2d611a559a Optimized structure for stream proxy
- Separated instance and config for stream proxy
2025-07-02 21:03:57 +08:00
6c5eba01c2 Update README.md
Added more contributors in community maintained section name list
2025-07-02 20:42:14 +08:00
f641797d10 Merge pull request #718 from jemmy1794/Stream-Proxy
Add Proxy Protocol V1 option in TCP Stream Proxy and update Stream Proxy UI
2025-07-02 20:40:08 +08:00
f92ff068f3 Added Proxy Protocol V1 to Stream Proxy UI
- Added a checkbox for Proxy Protocol V1.
- Modified related Config setting function.
2025-07-02 18:04:26 +08:00
b59ac47c8c Added Proxy Protocol V1 function.
- Added useProxyProtocol in ProxyRelayConfig
- Added writeProxyProtocolHeaderV1 function
2025-07-02 17:58:26 +08:00
8030f3d62a Fixed #688
- Added auto restart after config change in static web server
2025-06-30 20:34:42 +08:00
f8f623e3e4 Update .gitignore
Ignored dist folder
2025-06-28 17:00:31 +08:00
061839756c Merge pull request #711 from Morethanevil/main
Update CHANGELOG.md
2025-06-28 14:34:58 +08:00
1dcaa0c257 Update CHANGELOG.md 2025-06-28 08:31:20 +02:00
ffd3909964 Merge pull request #710 from tobychui/v3.2.4
V3.2.4 update
2025-06-28 10:06:23 +08:00
3ddccdffce Merge branch 'v3.2.4' of https://github.com/tobychui/zoraxy into v3.2.4 2025-06-27 22:02:29 +08:00
929d4cc82a Optimized SSO UI
- Added tab menu to SSO settings
2025-06-27 22:02:28 +08:00
4f1cd8a571 Merge pull request #705 from jemmy1794/v3.2.4
Fix: #659
2025-06-24 14:24:26 +08:00
f6b3656bb1 Fix: #659
Listen UDP port on (0.0.0.0)* address.
2025-06-24 13:10:58 +08:00
74a816216e Merge pull request #702 from PassiveLemon/main
Release type Docker workflows
2025-06-19 07:11:14 +08:00
4a093cf096 Merge branch 'tobychui:main' into main 2025-06-18 16:54:24 -04:00
68f9fccf3a refactor: release type workflows 2025-06-18 16:53:51 -04:00
f276040ad0 Added experimental fix for #695
Added prefix trim and location filter for oauth authrozied redirection
2025-06-16 21:21:50 +08:00
2f40593daf Updated version code 2025-06-16 21:12:49 +08:00
0b6dbd49bb Fixed #694
- Uncommented the delete proxy rule button
- Added redirection path escape in dpcore
2025-06-16 20:16:36 +08:00
37 changed files with 1768 additions and 2026 deletions

View File

@ -2,7 +2,7 @@ name: Build and push Docker image
on: on:
release: release:
types: [ published ] types: [ released, prereleased ]
jobs: jobs:
setup-build-push: setup-build-push:
@ -33,7 +33,8 @@ jobs:
run: | run: |
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/src/ cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/src/
- name: Build and push Docker image - name: Build and push Docker image (Release)
if: "!github.event.release.prerelease"
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: ./docker context: ./docker
@ -45,3 +46,15 @@ jobs:
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
- name: Build and push Docker image (Prerelease)
if: "github.event.release.prerelease"
uses: docker/build-push-action@v6
with:
context: ./docker
push: true
platforms: linux/amd64,linux/arm64
tags: |
zoraxydocker/zoraxy:${{ github.event.release.tag_name }}
cache-from: type=gha
cache-to: type=gha,mode=max

3
.gitignore vendored
View File

@ -56,4 +56,5 @@ log
tmp tmp
sys.* sys.*
www/html/index.html www/html/index.html
*.exe *.exe
/src/dist

View File

@ -1,3 +1,36 @@
# v3.2.4 28 Jun 2025
A big release since v3.1.9. Versions from 3.2.0 to 3.2.3 were prereleases.
+ Added Authentik support by [JokerQyou](https://github.com/tobychui/zoraxy/commits?author=JokerQyou)
+ Added pluginsystem and moved GAN and Zerotier to plugins
+ Add loopback detection [#573](https://github.com/tobychui/zoraxy/issues/573)
+ Fixed Dark theme not working with Advanced Option accordion [#591](https://github.com/tobychui/zoraxy/issues/591)
+ Update logger to include UserAgent by [Raithmir](https://github.com/Raithmir)
+ Fixed memory usage in UI [#600](https://github.com/tobychui/zoraxy/issues/600)
+ Added docker-compose.yml by [SamuelPalubaCZ](https://github.com/tobychui/zoraxy/commits?author=SamuelPalubaCZ)
+ Added more statistics for proxy hosts [#201](https://github.com/tobychui/zoraxy/issues/201) and [#608](https://github.com/tobychui/zoraxy/issues/608)
+ Fixed origin field in logs [#618](https://github.com/tobychui/zoraxy/issues/618)
+ Added FreeBSD support by Andreas Burri
+ Fixed HTTP proxy redirect [#626](https://github.com/tobychui/zoraxy/issues/626)
+ Fixed proxy handling #629](https://github.com/tobychui/zoraxy/issues/629)
+ Move Scope ID handling into CIDR check by [Nirostar](https://github.com/tobychui/zoraxy/commits?author=Nirostar)
+ Prevent the browser from filling the saved Zoraxy login account by [WHFo](https://github.com/tobychui/zoraxy/commits?author=WHFo)
+ Added port number and http proto to http proxy list link
+ Fixed headers for authelia by [james-d-elliott](https://github.com/tobychui/zoraxy/commits?author=james-d-elliott)
+ Refactored docker container list and UI improvements by [eyerrock](https://github.com/tobychui/zoraxy/commits?author=eyerrock)
+ Refactored Dockerfile by [PassiveLemon](https://github.com/tobychui/zoraxy/commits?author=PassiveLemon)
+ Added new HTTP proxy UI
+ Added inbound host name edit function
+ Added static web server option to disable listen to all interface
+ Merged SSO implementations (Oauth2) [#649](https://github.com/tobychui/zoraxy/pull/649)
+ Merged forward-auth optimization [#692(https://github.com/tobychui/zoraxy/pull/692)
+ Optimized SSO UI
+ Refactored docker image workflows by [PassiveLemon](https://github.com/tobychui/zoraxy/commits?author=PassiveLemon)
+ Added disable chunked transfer encoding checkbox (for upstreams that uses legacy HTTP implementations)
+ Bug fixes [#694](https://github.com/tobychui/zoraxy/issues/694), [#659](https://github.com/tobychui/zoraxy/issues/659) by [jemmy1794](https://github.com/tobychui/zoraxy/commits?author=jemmy1794), [#695](https://github.com/tobychui/zoraxy/issues/695)
# v3.1.9 1 Mar 2025 # v3.1.9 1 Mar 2025
+ Fixed netstat underflow bug + Fixed netstat underflow bug

View File

@ -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) - 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 ### Looking for Maintainer
- ACME DNS Challenge Module - ACME DNS Challenge Module

View File

@ -34,6 +34,7 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail) authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail)
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint) authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias) authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
authRouter.HandleFunc("/api/proxy/setTlsConfig", ReverseProxyHandleSetTlsConfig)
authRouter.HandleFunc("/api/proxy/setHostname", ReverseProxyHandleSetHostname) authRouter.HandleFunc("/api/proxy/setHostname", ReverseProxyHandleSetHostname)
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint) authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials) authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
@ -71,14 +72,20 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
// Register the APIs for TLS / SSL certificate management functions // Register the APIs for TLS / SSL certificate management functions
func RegisterTLSAPIs(authRouter *auth.RouterDef) { func RegisterTLSAPIs(authRouter *auth.RouterDef) {
//Global certificate settings
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy) authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest) authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
authRouter.HandleFunc("/api/cert/upload", handleCertUpload) authRouter.HandleFunc("/api/cert/resolve", handleCertTryResolve)
authRouter.HandleFunc("/api/cert/download", handleCertDownload) authRouter.HandleFunc("/api/cert/setPreferredCertificate", handleSetDomainPreferredCertificate)
authRouter.HandleFunc("/api/cert/list", handleListCertificate)
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains) //Certificate store functions
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck) authRouter.HandleFunc("/api/cert/upload", tlsCertManager.HandleCertUpload)
authRouter.HandleFunc("/api/cert/delete", handleCertRemove) 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 // Register the APIs for Authentication handlers like Forward Auth and OAUTH2

View File

@ -1,180 +1,14 @@
package main package main
import ( import (
"crypto/x509"
"encoding/json" "encoding/json"
"encoding/pem"
"fmt"
"io"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/utils" "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 // Handle front-end toggling TLS mode
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) { func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
currentTlsSetting := true //Default to true currentTlsSetting := true //Default to true
@ -185,11 +19,12 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
sysdb.Read("settings", "usetls", &currentTlsSetting) sysdb.Read("settings", "usetls", &currentTlsSetting)
} }
if r.Method == http.MethodGet { switch r.Method {
case http.MethodGet:
//Get the current status //Get the current status
js, _ := json.Marshal(currentTlsSetting) js, _ := json.Marshal(currentTlsSetting)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
} else if r.Method == http.MethodPost { case http.MethodPost:
newState, err := utils.PostBool(r, "set") newState, err := utils.PostBool(r, "set")
if err != nil { if err != nil {
utils.SendErrorResponse(w, "new state not set or invalid") utils.SendErrorResponse(w, "new state not set or invalid")
@ -205,7 +40,7 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
dynamicProxyRouter.UpdateTLSSetting(false) dynamicProxyRouter.UpdateTLSSetting(false)
} }
utils.SendOK(w) utils.SendOK(w)
} else { default:
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed) 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) js, _ := json.Marshal(reqLatestTLS)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
} else { } else {
if newState == "true" { switch newState {
case "true":
sysdb.Write("settings", "forceLatestTLS", true) sysdb.Write("settings", "forceLatestTLS", true)
SystemWideLogger.Println("Updating minimum TLS version to v1.2 or above") SystemWideLogger.Println("Updating minimum TLS version to v1.2 or above")
dynamicProxyRouter.UpdateTLSVersion(true) dynamicProxyRouter.UpdateTLSVersion(true)
} else if newState == "false" { case "false":
sysdb.Write("settings", "forceLatestTLS", false) sysdb.Write("settings", "forceLatestTLS", false)
SystemWideLogger.Println("Updating minimum TLS version to v1.0 or above") SystemWideLogger.Println("Updating minimum TLS version to v1.0 or above")
dynamicProxyRouter.UpdateTLSVersion(false) dynamicProxyRouter.UpdateTLSVersion(false)
} else { default:
utils.SendErrorResponse(w, "invalid state given") utils.SendErrorResponse(w, "invalid state given")
} }
} }
} }
// Handle download of the selected certificate func handleCertTryResolve(w http.ResponseWriter, r *http.Request) {
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 // get the domain
domain, err := utils.GetPara(r, "domain") domain, err := utils.GetPara(r, "domain")
if err != nil { if err != nil {
//Assume localhost utils.SendErrorResponse(w, "invalid domain given")
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 return
} }
// parse multipart form data // get the proxy rule, the pass in domain value must be root or matching domain
err = r.ParseMultipartForm(10 << 20) // 10 MB proxyRule, err := dynamicProxyRouter.GetProxyEndpointById(domain, false)
if err != nil { if err != nil {
http.Error(w, "Failed to parse form data", http.StatusBadRequest) //Try to resolve the domain via alias
return 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 // list all the alias domains for this rule
file, _, err := r.FormFile("file") allDomains := []string{proxyRule.RootOrMatchingDomain}
if err != nil { aliasDomains := []string{}
http.Error(w, "Failed to get file", http.StatusBadRequest) for _, alias := range proxyRule.MatchingDomainAlias {
return if alias != "" {
} aliasDomains = append(aliasDomains, alias)
defer file.Close() allDomains = append(allDomains, alias)
}
// 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 // Try to resolve the domain
tlsCertManager.UpdateLoadedCertList() 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 //Make sure pubkey and private key are not empty
fmt.Fprintln(w, "File upload successful!") 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 handleSetDomainPreferredCertificate(w http.ResponseWriter, r *http.Request) {
func handleCertRemove(w http.ResponseWriter, r *http.Request) { //Get the domain
domain, err := utils.PostPara(r, "domain") domain, err := utils.PostPara(r, "domain")
if err != nil { if err != nil {
utils.SendErrorResponse(w, "invalid domain given") utils.SendErrorResponse(w, "invalid domain given")
return return
} }
err = tlsCertManager.RemoveCert(domain)
//Get the certificate name
certName, err := utils.PostPara(r, "certname")
if err != nil { 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)
} }

View File

@ -15,6 +15,7 @@ import (
"imuslab.com/zoraxy/mod/dynamicproxy" "imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
@ -59,12 +60,18 @@ func LoadReverseProxyConfig(configFilepath string) error {
thisConfigEndpoint.Tags = []string{} 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 //Matching domain not set. Assume root
if thisConfigEndpoint.RootOrMatchingDomain == "" { if thisConfigEndpoint.RootOrMatchingDomain == "" {
thisConfigEndpoint.RootOrMatchingDomain = "/" thisConfigEndpoint.RootOrMatchingDomain = "/"
} }
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeRoot { switch thisConfigEndpoint.ProxyType {
case dynamicproxy.ProxyTypeRoot:
//This is a root config file //This is a root config file
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint) rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil { if err != nil {
@ -73,7 +80,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint) dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint)
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeHost { case dynamicproxy.ProxyTypeHost:
//This is a host config file //This is a host config file
readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint) readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil { if err != nil {
@ -81,7 +88,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
} }
dynamicProxyRouter.AddProxyRouteToRuntime(readyProxyEndpoint) dynamicProxyRouter.AddProxyRouteToRuntime(readyProxyEndpoint)
} else { default:
return errors.New("not supported proxy type") return errors.New("not supported proxy type")
} }

View File

@ -44,7 +44,7 @@ import (
const ( const (
/* Build Constants */ /* Build Constants */
SYSTEM_NAME = "Zoraxy" SYSTEM_NAME = "Zoraxy"
SYSTEM_VERSION = "3.2.3" SYSTEM_VERSION = "3.2.4"
DEVELOPMENT_BUILD = false DEVELOPMENT_BUILD = false
/* System Constants */ /* System Constants */

View File

@ -58,11 +58,20 @@ func NewAuthRouter(options *AuthRouterOptions) *AuthRouter {
options.Database.Read(DatabaseTable, DatabaseKeyRequestIncludedCookies, &requestIncludedCookies) options.Database.Read(DatabaseTable, DatabaseKeyRequestIncludedCookies, &requestIncludedCookies)
options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, &requestExcludedCookies) options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, &requestExcludedCookies)
options.ResponseHeaders = strings.Split(responseHeaders, ",") // Helper function to clean empty strings from split results
options.ResponseClientHeaders = strings.Split(responseClientHeaders, ",") cleanSplit := func(s string) []string {
options.RequestHeaders = strings.Split(requestHeaders, ",") if s == "" {
options.RequestIncludedCookies = strings.Split(requestIncludedCookies, ",") return nil
options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",") }
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{ return &AuthRouter{
client: &http.Client{ client: &http.Client{

View File

@ -4,13 +4,14 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"net/http"
"net/url"
"strings"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger" "imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
"net/http"
"net/url"
"strings"
) )
type OAuth2RouterOptions struct { type OAuth2RouterOptions struct {
@ -250,7 +251,19 @@ func (ar *OAuth2Router) HandleOAuth2Auth(w http.ResponseWriter, r *http.Request)
cookie.SameSite = http.SameSiteLaxMode cookie.SameSite = http.SameSiteLaxMode
} }
w.Header().Add("Set-Cookie", cookie.String()) w.Header().Add("Set-Cookie", cookie.String())
http.Redirect(w, r, state, http.StatusTemporaryRedirect)
//Fix for #695
location := strings.TrimPrefix(state, "/internal/")
//Check if the location starts with http:// or https://. if yes, this is full URL
decodedLocation, err := url.PathUnescape(location)
if err == nil && (strings.HasPrefix(decodedLocation, "http://") || strings.HasPrefix(decodedLocation, "https://")) {
//Redirect to the full URL
http.Redirect(w, r, decodedLocation, http.StatusTemporaryRedirect)
} else {
//Redirect to a relative path
http.Redirect(w, r, state, http.StatusTemporaryRedirect)
}
return errors.New("authorized") return errors.New("authorized")
} }
unauthorized := false unauthorized := false

View File

@ -61,7 +61,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
hostPath := strings.Split(r.Host, ":") hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0] domainOnly = hostPath[0]
} }
sep := h.Parent.getProxyEndpointFromHostname(domainOnly) sep := h.Parent.GetProxyEndpointFromHostname(domainOnly)
if sep != nil && !sep.Disabled { if sep != nil && !sep.Disabled {
//Matching proxy rule found //Matching proxy rule found
//Access Check (blacklist / whitelist) //Access Check (blacklist / whitelist)

View 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
}

View File

@ -330,7 +330,10 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
locationRewrite := res.Header.Get("Location") locationRewrite := res.Header.Get("Location")
originLocation := res.Header.Get("Location") originLocation := res.Header.Get("Location")
res.Header.Set("zr-origin-location", originLocation) res.Header.Set("zr-origin-location", originLocation)
decodedOriginLocation, err := url.PathUnescape(originLocation)
if err == nil {
originLocation = decodedOriginLocation
}
if strings.HasPrefix(originLocation, "http://") || strings.HasPrefix(originLocation, "https://") { if strings.HasPrefix(originLocation, "http://") || strings.HasPrefix(originLocation, "https://") {
//Full path //Full path
//Replace the forwarded target with expected Host //Replace the forwarded target with expected Host

View File

@ -111,7 +111,7 @@ func (router *Router) StartProxyService() error {
hostPath := strings.Split(r.Host, ":") hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0] domainOnly = hostPath[0]
} }
sep := router.getProxyEndpointFromHostname(domainOnly) sep := router.GetProxyEndpointFromHostname(domainOnly)
if sep != nil && sep.BypassGlobalTLS { if sep != nil && sep.BypassGlobalTLS {
//Allow routing via non-TLS handler //Allow routing via non-TLS handler
originalHostHeader := r.Host originalHostHeader := r.Host
@ -335,7 +335,7 @@ func (router *Router) IsProxiedSubdomain(r *http.Request) bool {
hostname = r.Host hostname = r.Host
} }
hostname = strings.Split(hostname, ":")[0] hostname = strings.Split(hostname, ":")[0]
subdEndpoint := router.getProxyEndpointFromHostname(hostname) subdEndpoint := router.GetProxyEndpointFromHostname(hostname)
return subdEndpoint != nil return subdEndpoint != nil
} }

View 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])
}

View File

@ -34,7 +34,7 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
} }
// Get the proxy endpoint from hostname, which might includes checking of wildcard certificates // 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 var targetSubdomainEndpoint *ProxyEndpoint = nil
hostname = strings.ToLower(hostname) hostname = strings.ToLower(hostname)
ep, ok := router.ProxyEndpoints.Load(hostname) ep, ok := router.ProxyEndpoints.Load(hostname)
@ -63,7 +63,7 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi
} }
//Wildcard not match. Check for alias //Wildcard not match. Check for alias
if ep.MatchingDomainAlias != nil && len(ep.MatchingDomainAlias) > 0 { if len(ep.MatchingDomainAlias) > 0 {
for _, aliasDomain := range ep.MatchingDomainAlias { for _, aliasDomain := range ep.MatchingDomainAlias {
match, err := filepath.Match(aliasDomain, hostname) match, err := filepath.Match(aliasDomain, hostname)
if err != nil { if err != nil {

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/utils"
) )
/* /*
@ -105,3 +106,49 @@ func (router *Router) RemoveProxyEndpointByRootname(rootnameOrMatchingDomain str
return targetEpt.Remove() 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")
}

View File

@ -75,16 +75,20 @@ type RouterOption struct {
/* Router Object */ /* Router Object */
type Router struct { type Router struct {
Option *RouterOption Option *RouterOption
ProxyEndpoints *sync.Map //Map of ProxyEndpoint objects, each ProxyEndpoint object is a routing rule that handle incoming requests 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 Running bool //If the router is running
Root *ProxyEndpoint //Root proxy endpoint, default site Root *ProxyEndpoint //Root proxy endpoint, default site
mux http.Handler //HTTP handler
server *http.Server //HTTP server /* Internals */
tlsListener net.Listener //TLS listener, handle SNI routing mux http.Handler //HTTP handler
loadBalancer *loadbalance.RouteManager //Load balancer routing manager server *http.Server //HTTP server
routingRules []*RoutingRule //Special routing rules, handle high priority routing like ACME request handling 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 rateLimterStop chan bool //Stop channel for rate limiter
rateLimitCounter RequestCountPerIpTable //Request counter for rate limter rateLimitCounter RequestCountPerIpTable //Request counter for rate limter
} }
@ -175,7 +179,8 @@ type ProxyEndpoint struct {
Disabled bool //If the rule is disabled Disabled bool //If the rule is disabled
//Inbound TLS/SSL Related //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 //Virtual Directories
VirtualDirectories []*VirtualDirectoryEndpoint VirtualDirectories []*VirtualDirectoryEndpoint

View File

@ -47,15 +47,19 @@ func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) {
useTCP, _ := utils.PostBool(r, "useTCP") useTCP, _ := utils.PostBool(r, "useTCP")
useUDP, _ := utils.PostBool(r, "useUDP") useUDP, _ := utils.PostBool(r, "useUDP")
useProxyProtocol, _ := utils.PostBool(r, "useProxyProtocol")
enableLogging, _ := utils.PostBool(r, "enableLogging")
//Create the target config //Create the target config
newConfigUUID := m.NewConfig(&ProxyRelayOptions{ newConfigUUID := m.NewConfig(&ProxyRelayOptions{
Name: name, Name: name,
ListeningAddr: strings.TrimSpace(listenAddr), ListeningAddr: strings.TrimSpace(listenAddr),
ProxyAddr: strings.TrimSpace(proxyAddr), ProxyAddr: strings.TrimSpace(proxyAddr),
Timeout: timeout, Timeout: timeout,
UseTCP: useTCP, UseTCP: useTCP,
UseUDP: useUDP, UseUDP: useUDP,
UseProxyProtocol: useProxyProtocol,
EnableLogging: enableLogging,
}) })
js, _ := json.Marshal(newConfigUUID) js, _ := json.Marshal(newConfigUUID)
@ -75,6 +79,8 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request)
proxyAddr, _ := utils.PostPara(r, "proxyAddr") proxyAddr, _ := utils.PostPara(r, "proxyAddr")
useTCP, _ := utils.PostBool(r, "useTCP") useTCP, _ := utils.PostBool(r, "useTCP")
useUDP, _ := utils.PostBool(r, "useUDP") useUDP, _ := utils.PostBool(r, "useUDP")
useProxyProtocol, _ := utils.PostBool(r, "useProxyProtocol")
enableLogging, _ := utils.PostBool(r, "enableLogging")
newTimeoutStr, _ := utils.PostPara(r, "timeout") newTimeoutStr, _ := utils.PostPara(r, "timeout")
newTimeout := -1 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 // 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 { if err != nil {
utils.SendErrorResponse(w, err.Error()) utils.SendErrorResponse(w, err.Error())
return return

View 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()
}

View File

@ -8,7 +8,6 @@ import (
"path/filepath" "path/filepath"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"imuslab.com/zoraxy/mod/info/logger" "imuslab.com/zoraxy/mod/info/logger"
@ -24,24 +23,44 @@ import (
*/ */
type ProxyRelayOptions struct { type ProxyRelayOptions struct {
Name string Name string
ListeningAddr string ListeningAddr string
ProxyAddr string ProxyAddr string
Timeout int Timeout int
UseTCP bool UseTCP bool
UseUDP bool UseUDP bool
UseProxyProtocol bool
EnableLogging bool
} }
type ProxyRelayConfig struct { // ProxyRuleUpdateConfig is used to update the proxy rule config
UUID string //A UUIDv4 representing this config type ProxyRuleUpdateConfig struct {
Name string //Name of the config InstanceUUID string //The target instance UUID to update
Running bool //Status, read only NewName string //New name for the instance, leave empty for no change
AutoStart bool //If the service suppose to started automatically NewListeningAddr string //New listening address, leave empty for no change
ListeningAddress string //Listening Address, usually 127.0.0.1:port NewProxyAddr string //New proxy target address, leave empty for no change
ProxyTargetAddr string //Proxy target address UseTCP bool //Enable TCP proxy, default to false
UseTCP bool //Enable TCP proxy UseUDP bool //Enable UDP proxy, default to false
UseUDP bool //Enable UDP proxy UseProxyProtocol bool //Enable Proxy Protocol, default to false
Timeout int //Timeout for connection in sec 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 tcpStopChan chan bool //Stop channel for TCP listener
udpStopChan chan bool //Stop channel for UDP listener udpStopChan chan bool //Stop channel for UDP listener
aTobAccumulatedByteTransfer atomic.Int64 //Accumulated byte transfer from A to B aTobAccumulatedByteTransfer atomic.Int64 //Accumulated byte transfer from A to B
@ -60,13 +79,14 @@ type Options struct {
type Manager struct { type Manager struct {
//Config and stores //Config and stores
Options *Options Options *Options
Configs []*ProxyRelayConfig Configs []*ProxyRelayInstance
//Realtime Statistics //Realtime Statistics
Connections int //currently connected connect counts Connections int //currently connected connect counts
} }
// NewStreamProxy creates a new stream proxy manager with the given options
func NewStreamProxy(options *Options) (*Manager, error) { func NewStreamProxy(options *Options) (*Manager, error) {
if !utils.FileExists(options.ConfigStore) { if !utils.FileExists(options.ConfigStore) {
err := os.MkdirAll(options.ConfigStore, 0775) err := os.MkdirAll(options.ConfigStore, 0775)
@ -76,7 +96,7 @@ func NewStreamProxy(options *Options) (*Manager, error) {
} }
//Load relay configs from db //Load relay configs from db
previousRules := []*ProxyRelayConfig{} previousRules := []*ProxyRelayInstance{}
streamProxyConfigFiles, err := filepath.Glob(options.ConfigStore + "/*.config") streamProxyConfigFiles, err := filepath.Glob(options.ConfigStore + "/*.config")
if err != nil { if err != nil {
return nil, err return nil, err
@ -89,7 +109,7 @@ func NewStreamProxy(options *Options) (*Manager, error) {
options.Logger.PrintAndLog("stream-prox", "Read stream proxy config failed", err) options.Logger.PrintAndLog("stream-prox", "Read stream proxy config failed", err)
continue continue
} }
thisRelayConfig := &ProxyRelayConfig{} thisRelayConfig := &ProxyRelayInstance{}
err = json.Unmarshal(configBytes, thisRelayConfig) err = json.Unmarshal(configBytes, thisRelayConfig)
if err != nil { if err != nil {
options.Logger.PrintAndLog("stream-prox", "Unmarshal stream proxy config failed", err) 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) 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 { func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
//Generate two zero value for atomic int64 //Generate two zero value for atomic int64
aAcc := atomic.Int64{} aAcc := atomic.Int64{}
@ -150,13 +171,15 @@ func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
bAcc.Store(0) bAcc.Store(0)
//Generate a new config from options //Generate a new config from options
configUUID := uuid.New().String() configUUID := uuid.New().String()
thisConfig := ProxyRelayConfig{ thisConfig := ProxyRelayInstance{
UUID: configUUID, UUID: configUUID,
Name: config.Name, Name: config.Name,
ListeningAddress: config.ListeningAddr, ListeningAddress: config.ListeningAddr,
ProxyTargetAddr: config.ProxyAddr, ProxyTargetAddr: config.ProxyAddr,
UseTCP: config.UseTCP, UseTCP: config.UseTCP,
UseUDP: config.UseUDP, UseUDP: config.UseUDP,
UseProxyProtocol: config.UseProxyProtocol,
EnableLogging: config.EnableLogging,
Timeout: config.Timeout, Timeout: config.Timeout,
tcpStopChan: nil, tcpStopChan: nil,
udpStopChan: nil, udpStopChan: nil,
@ -170,7 +193,7 @@ func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
return configUUID 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 // Find and return the config with the specified UUID
for _, config := range m.Configs { for _, config := range m.Configs {
if config.UUID == configUUID { 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 // 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 // Find the config with the specified UUID
foundConfig, err := m.GetConfigByUUID(configUUID) foundConfig, err := m.GetConfigByUUID(newConfig.InstanceUUID)
if err != nil { if err != nil {
return err return err
} }
// Validate and update the fields // Validate and update the fields
if newName != "" { if newConfig.NewName != "" {
foundConfig.Name = newName foundConfig.Name = newConfig.NewName
} }
if newListeningAddr != "" { if newConfig.NewListeningAddr != "" {
foundConfig.ListeningAddress = newListeningAddr foundConfig.ListeningAddress = newConfig.NewListeningAddr
} }
if newProxyAddr != "" { if newConfig.NewProxyAddr != "" {
foundConfig.ProxyTargetAddr = newProxyAddr foundConfig.ProxyTargetAddr = newConfig.NewProxyAddr
} }
foundConfig.UseTCP = useTCP foundConfig.UseTCP = newConfig.UseTCP
foundConfig.UseUDP = useUDP foundConfig.UseUDP = newConfig.UseUDP
foundConfig.UseProxyProtocol = newConfig.UseProxyProtocol
foundConfig.EnableLogging = newConfig.EnableLogging
if newTimeout != -1 { if newConfig.NewTimeout != -1 {
if newTimeout < 0 { if newConfig.NewTimeout < 0 {
return errors.New("invalid timeout value given") return errors.New("invalid timeout value given")
} }
foundConfig.Timeout = newTimeout foundConfig.Timeout = newConfig.NewTimeout
} }
m.SaveConfigToDatabase() m.SaveConfigToDatabase()
@ -215,12 +240,11 @@ func (m *Manager) EditConfig(configUUID string, newName string, newListeningAddr
if foundConfig.IsRunning() { if foundConfig.IsRunning() {
foundConfig.Restart() foundConfig.Restart()
} }
return nil return nil
} }
// Remove the config from file by UUID
func (m *Manager) RemoveConfig(configUUID string) error { func (m *Manager) RemoveConfig(configUUID string) error {
//Remove the config from file
err := os.Remove(filepath.Join(m.Options.ConfigStore, configUUID+".config")) err := os.Remove(filepath.Join(m.Options.ConfigStore, configUUID+".config"))
if err != nil { if err != nil {
return err 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()
}

View File

@ -12,7 +12,7 @@ func TestPort2Port(t *testing.T) {
stopChan := make(chan bool) stopChan := make(chan bool)
// Create a ProxyRelayConfig with dummy values // Create a ProxyRelayConfig with dummy values
config := &streamproxy.ProxyRelayConfig{ config := &streamproxy.ProxyRelayInstance{
Timeout: 1, Timeout: 1,
} }

View File

@ -2,6 +2,7 @@ package streamproxy
import ( import (
"errors" "errors"
"fmt"
"io" "io"
"log" "log"
"net" "net"
@ -30,48 +31,67 @@ func isValidPort(port string) bool {
return true 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) n, err := io.Copy(conn1, conn2)
if err != nil { if err != nil {
return return
} }
accumulator.Add(n) //Add to accumulator accumulator.Add(n) //Add to accumulator
conn1.Close() 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() //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() wg.Done()
} }
func forward(conn1 net.Conn, conn2 net.Conn, aTob *atomic.Int64, bToa *atomic.Int64) { func writeProxyProtocolHeaderV1(dst net.Conn, src net.Conn) error {
log.Printf("[+] start transmit. [%s],[%s] <-> [%s],[%s] \n", conn1.LocalAddr().String(), conn1.RemoteAddr().String(), conn2.LocalAddr().String(), conn2.RemoteAddr().String()) 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 var wg sync.WaitGroup
// wait tow goroutines
wg.Add(2) wg.Add(2)
go connCopy(conn1, conn2, &wg, aTob) go c.connCopy(conn1, conn2, &wg, aTob)
go connCopy(conn2, conn1, &wg, bToa) go c.connCopy(conn2, conn1, &wg, bToa)
//blocking when the wg is locked
wg.Wait() 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() conn, err := listener.Accept()
if err != nil { if err != nil {
return nil, err 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 addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
if !c.parent.Options.AccessControlHandler(conn) { if !c.parent.Options.AccessControlHandler(conn) {
time.Sleep(300 * time.Millisecond) time.Sleep(300 * time.Millisecond)
conn.Close() 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") 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()+"]") c.LogMsg("[√] accept a new client. remote address:["+conn.RemoteAddr().String()+"], local address:["+conn.LocalAddr().String()+"]", nil)
return conn, err return conn, nil
} }
func startListener(address string) (net.Listener, error) { func startListener(address string) (net.Listener, error) {
@ -92,7 +112,7 @@ func startListener(address string) (net.Listener, error) {
portA -> server portA -> server
server -> portB 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 listenerStartingAddr := allowPort
if isValidPort(allowPort) { if isValidPort(allowPort) {
//number only, e.g. 8080 //number only, e.g. 8080
@ -112,7 +132,7 @@ func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, sto
//Start stop handler //Start stop handler
go func() { go func() {
<-stopChan <-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() server.Close()
}() }()
@ -129,18 +149,32 @@ func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, sto
} }
go func(targetAddress string) { go func(targetAddress string) {
log.Println("[+]", "start connect host:["+targetAddress+"]") c.LogMsg("[+] start connect host:["+targetAddress+"]", nil)
target, err := net.Dial("tcp", targetAddress) target, err := net.Dial("tcp", targetAddress)
if err != nil { if err != nil {
// temporarily unavailable, don't use fatal. // 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() 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) time.Sleep(time.Duration(c.Timeout) * time.Second)
return return
} }
log.Println("[→]", "connect target address ["+targetAddress+"] success.") c.LogMsg("[→] connect target address ["+targetAddress+"] success.", nil)
forward(target, conn, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
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) }(targetAddress)
} }
} }

View File

@ -53,7 +53,7 @@ func initUDPConnections(listenAddr string, targetAddress string) (*net.UDPConn,
} }
// Go routine which manages connection from server to single client // 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 var buffer [1500]byte
for { for {
// Read from server // Read from server
@ -74,7 +74,7 @@ func (c *ProxyRelayConfig) RunUDPConnectionRelay(conn *udpClientServerConn, lise
} }
// Close all connections that waiting for read from server // 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 { c.udpClientMap.Range(func(clientAddr, clientServerConn interface{}) bool {
conn := clientServerConn.(*udpClientServerConn) conn := clientServerConn.(*udpClientServerConn)
conn.ServerConn.Close() 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 //By default the incoming listen Address is int
//We need to add the loopback address into it //We need to add the loopback address into it
if isValidPort(address1) { if isValidPort(address1) {
@ -90,8 +90,8 @@ func (c *ProxyRelayConfig) ForwardUDP(address1, address2 string, stopChan chan b
address1 = ":" + address1 address1 = ":" + address1
} }
if strings.HasPrefix(address1, ":") { if strings.HasPrefix(address1, ":") {
//Prepend 127.0.0.1 to the address //Prepend 0.0.0.0 to the address
address1 = "127.0.0.1" + address1 address1 = "0.0.0.0" + address1
} }
lisener, targetAddr, err := initUDPConnections(address1, address2) lisener, targetAddr, err := initUDPConnections(address1, address2)
@ -138,12 +138,12 @@ func (c *ProxyRelayConfig) ForwardUDP(address1, address2 string, stopChan chan b
continue continue
} }
c.udpClientMap.Store(saddr, conn) 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 // Fire up routine to manage new connection
go c.RunUDPConnectionRelay(conn, lisener) go c.RunUDPConnectionRelay(conn, lisener)
} else { } else {
log.Println("[UDP] Found connection for client " + saddr) c.LogMsg("[UDP] Found connection for client "+saddr, nil)
conn = rawConn.(*udpClientServerConn) conn = rawConn.(*udpClientServerConn)
} }

View 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
View 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)
}

View File

@ -43,3 +43,30 @@ func matchClosestDomainCertificate(subdomain string, domains []string) string {
return matchingDomain 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
}

View File

@ -20,17 +20,26 @@ type CertCache struct {
PriKey string 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 { type Manager struct {
CertStore string //Path where all the certs are stored CertStore string //Path where all the certs are stored
LoadedCerts []*CertCache //A list of loaded certs LoadedCerts []*CertCache //A list of loaded certs
Logger *logger.Logger //System wide logger for debug mesage 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 //go:embed localhost.pem localhost.key
var buildinCertStore embed.FS 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) { if !utils.FileExists(certStore) {
os.MkdirAll(certStore, 0775) os.MkdirAll(certStore, 0775)
} }
@ -50,10 +59,10 @@ func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager,
} }
thisManager := Manager{ thisManager := Manager{
CertStore: certStore, CertStore: certStore,
LoadedCerts: []*CertCache{}, LoadedCerts: []*CertCache{},
verbal: verbal, hostSpecificTlsBehavior: defaultHostSpecificTlsBehavior, //Default to no SNI and no auto HTTPS
Logger: logger, Logger: logger,
} }
err := thisManager.UpdateLoadedCertList() err := thisManager.UpdateLoadedCertList()
@ -64,6 +73,25 @@ func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager,
return &thisManager, nil 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 // Update domain mapping from file
func (m *Manager) UpdateLoadedCertList() error { func (m *Manager) UpdateLoadedCertList() error {
//Get a list of certificates from file //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 // Get a certificate from disk where its certificate matches with the helloinfo
func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) { func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) {
//Check if the domain corrisponding cert exists //Look for the certificate by hostname
pubKey := "./tmp/localhost.pem" pubKey, priKey, err := m.GetCertificateByHostname(helloInfo.ServerName)
priKey := "./tmp/localhost.key" if err != nil {
m.Logger.PrintAndLog("tls-router", "Failed to get certificate for "+helloInfo.ServerName, err)
if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".pem")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) { return nil, err
//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")
}
} }
//Load the cert and serve it //Load the cert and serve it
@ -190,6 +205,55 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err
return &cer, nil 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 // Check if both the default cert public key and private key exists
func (m *Manager) DefaultCertExists() bool { func (m *Manager) DefaultCertExists() bool {
return utils.FileExists(filepath.Join(m.CertStore, "default.pem")) && utils.FileExists(filepath.Join(m.CertStore, "default.key")) 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 //Update the cert list
m.UpdateLoadedCertList() m.UpdateLoadedCertList()
return nil return nil
} }

View File

@ -69,6 +69,12 @@ func (ws *WebServer) HandlePortChange(w http.ResponseWriter, r *http.Request) {
return 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)) err = ws.ChangePort(strconv.Itoa(newPort))
if err != nil { if err != nil {
utils.SendErrorResponse(w, err.Error()) 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") utils.SendErrorResponse(w, "unable to save setting")
return return
} }
// Update the option in the web server instance
ws.option.DisableListenToAllInterface = disableListen 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) utils.SendOK(w)
} }

View File

@ -210,6 +210,27 @@ func (ws *WebServer) Stop() error {
return nil 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. // UpdateDirectoryListing enables or disables directory listing.
func (ws *WebServer) UpdateDirectoryListing(enable bool) { func (ws *WebServer) UpdateDirectoryListing(enable bool) {
ws.option.EnableDirectoryListing = enable ws.option.EnableDirectoryListing = enable

View File

@ -15,6 +15,7 @@ import (
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite" "imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/netutils" "imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/uptime" "imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
@ -139,7 +140,7 @@ func ReverseProxtInit() {
err := LoadReverseProxyConfig(conf) err := LoadReverseProxyConfig(conf)
if err != nil { if err != nil {
SystemWideLogger.PrintAndLog("proxy-config", "Failed to load config file: "+filepath.Base(conf), err) 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 tags = filteredTags
var proxyEndpointCreated *dynamicproxy.ProxyEndpoint var proxyEndpointCreated *dynamicproxy.ProxyEndpoint
if eptype == "host" { switch eptype {
case "host":
rootOrMatchingDomain, err := utils.PostPara(r, "rootname") rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
if err != nil { if err != nil {
utils.SendErrorResponse(w, "hostname not defined") utils.SendErrorResponse(w, "hostname not defined")
@ -415,7 +417,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
dynamicProxyRouter.AddProxyRouteToRuntime(preparedEndpoint) dynamicProxyRouter.AddProxyRouteToRuntime(preparedEndpoint)
proxyEndpointCreated = &thisProxyEndpoint proxyEndpointCreated = &thisProxyEndpoint
} else if eptype == "root" { case "root":
//Get the default site options and target //Get the default site options and target
dsOptString, err := utils.PostPara(r, "defaultSiteOpt") dsOptString, err := utils.PostPara(r, "defaultSiteOpt")
if err != nil { if err != nil {
@ -469,7 +471,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
return return
} }
proxyEndpointCreated = &rootRoutingEndpoint proxyEndpointCreated = &rootRoutingEndpoint
} else { default:
//Invalid eptype //Invalid eptype
utils.SendErrorResponse(w, "invalid endpoint type") utils.SendErrorResponse(w, "invalid endpoint type")
return return
@ -677,6 +679,70 @@ func ReverseProxyHandleAlias(w http.ResponseWriter, r *http.Request) {
utils.SendOK(w) 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) { func ReverseProxyHandleSetHostname(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
utils.SendErrorResponse(w, "Method not supported") 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) { func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
js, err := json.Marshal(dynamicProxyRouter) js, err := json.Marshal(dynamicProxyRouter)
if err != nil { if err != nil {
SystemWideLogger.PrintAndLog("proxy-config", "Unable to marshal status data", err)
utils.SendErrorResponse(w, "Unable to marshal status data") utils.SendErrorResponse(w, "Unable to marshal status data")
return return
} }

View File

@ -1,7 +1,6 @@
package main package main
import ( import (
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -10,6 +9,8 @@ import (
"strings" "strings"
"time" "time"
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
"imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/acme"
@ -99,7 +100,7 @@ func startupSequence() {
}) })
//Create a TLS certificate manager //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 { if err != nil {
panic(err) panic(err)
} }
@ -366,6 +367,9 @@ func finalSequence() {
//Inject routing rules //Inject routing rules
registerBuildInRoutingRules() registerBuildInRoutingRules()
//Set the host specific TLS behavior resolver for resolving TLS behavior for each hostname
tlsCertManager.SetHostSpecificTlsBehavior(dynamicProxyRouter.ResolveHostSpecificTlsBehaviorForHostname)
} }
/* Shutdown Sequence */ /* Shutdown Sequence */

View File

@ -203,7 +203,7 @@
<th>Destination</th> <th>Destination</th>
<th>Virtual Directory</th> <th>Virtual Directory</th>
<th class="no-sort">Tags</th> <th class="no-sort">Tags</th>
<th class="no-sort" style="width:50px; cursor: default !important;"></th> <th class="no-sort" style="width:100px; cursor: default !important;"></th>
</tr> </tr>
</thead> </thead>
<tbody id="httpProxyList"> <tbody id="httpProxyList">
@ -338,10 +338,45 @@
<!-- TLS / SSL --> <!-- TLS / SSL -->
<div class="rpconfig_content" rpcfg="ssl"> <div class="rpconfig_content" rpcfg="ssl">
<div class="ui segment"> <div class="ui segment">
<p>Work In Progress <br> <p>The table below shows which certificate will be served by Zoraxy when a client request the following hostnames.</p>
Please use the outer-most menu TLS / SSL tab for now. </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> <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 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>
</div> </div>
<!-- Custom Headers --> <!-- Custom Headers -->
@ -551,6 +586,28 @@
aliasDomains += `</small><br>`; 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 //Build tag list
let tagList = renderTagList(subd); let tagList = renderTagList(subd);
let tagListEmpty = (subd.Tags.length == 0); let tagListEmpty = (subd.Tags.length == 0);
@ -567,7 +624,7 @@
${aliasDomains} ${aliasDomains}
<small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small> <small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small>
</td> </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"> <div class="upstreamList">
${upstreams} ${upstreams}
</div> </div>
@ -588,7 +645,7 @@
</td> --> </td> -->
<td class="center aligned ignoremw" editable="true" datatype="action" data-label=""> <td class="center aligned ignoremw" editable="true" datatype="action" data-label="">
<button title="Edit Proxy Rule" class="ui circular small basic icon button editBtn inlineEditActionBtn" onclick='editEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="ellipsis vertical icon"></i></button> <button title="Edit Proxy Rule" class="ui circular small basic icon button editBtn inlineEditActionBtn" onclick='editEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="ellipsis vertical icon"></i></button>
<!-- <button title="Remove Proxy Rule" class="ui circular mini red basic icon button inlineEditActionBtn" onclick='deleteEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="trash icon"></i></button> --> <button title="Remove Proxy Rule" class="ui circular mini red basic icon button inlineEditActionBtn" onclick='deleteEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="trash icon"></i></button>
</td> </td>
</tr>`); </tr>`);
}); });
@ -711,6 +768,112 @@
$("#httpProxyList").find(".editBtn").removeClass("disabled"); $("#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){ function saveProxyInlineEdit(uuid){
let editor = $("#httprpEditModal"); let editor = $("#httprpEditModal");
@ -857,6 +1020,29 @@
renewCertificate(renewDomainKey, false, btn); 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 */ /* Tags & Search */
function handleSearchInput(event){ function handleSearchInput(event){
if (event.key == "Escape"){ if (event.key == "Escape"){
@ -985,6 +1171,28 @@
return subd; 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 //Initialize the http proxy rule editor
function initHttpProxyRuleEditorModal(rulepayload){ function initHttpProxyRuleEditorModal(rulepayload){
let subd = JSON.parse(JSON.stringify(rulepayload)); let subd = JSON.parse(JSON.stringify(rulepayload));
@ -1086,39 +1294,6 @@
}); });
editor.find(".downstream_alias_hostname").html(aliasHTML); 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 ------------ */ /* ------------ Upstreams ------------ */
editor.find(".upstream_list").html(renderUpstreamList(subd)); editor.find(".upstream_list").html(renderUpstreamList(subd));
@ -1148,6 +1323,8 @@
editor.find(".vdir_list").html(renderVirtualDirectoryList(subd)); editor.find(".vdir_list").html(renderVirtualDirectoryList(subd));
editor.find(".editVdirBtn").off("click").on("click", function(){ editor.find(".editVdirBtn").off("click").on("click", function(){
quickEditVdir(uuid); quickEditVdir(uuid);
//Temporary restore scroll
$("body").css("overflow", "auto");
}); });
/* ------------ Alias ------------ */ /* ------------ Alias ------------ */
@ -1245,6 +1422,60 @@
editor.find(".RateLimit").off("change").on("change", rateLimitChangeEvent); editor.find(".RateLimit").off("change").on("change", rateLimitChangeEvent);
/* ------------ TLS ------------ */ /* ------------ 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 ------------ */ /* ------------ Tags ------------ */
(()=>{ (()=>{
@ -1308,7 +1539,6 @@
}); });
} }
/* /*
Page Initialization Functions Page Initialization Functions
*/ */
@ -1333,7 +1563,9 @@
// there is a chance where the user has modified the Vdir // there is a chance where the user has modified the Vdir
// we need to get the latest setting from server side and // we need to get the latest setting from server side and
// render it again // render it again
updateVdirInProxyEditor(); resyncProxyEditorConfig();
window.scrollTo(0, 0);
$("body").css("overflow", "hidden");
} else { } else {
listProxyEndpoints(); listProxyEndpoints();
//Reset the tag filter //Reset the tag filter

File diff suppressed because it is too large Load Diff

View File

@ -3,18 +3,15 @@
<h2>SSO</h2> <h2>SSO</h2>
<p>Single Sign-On (SSO) and authentication providers settings </p> <p>Single Sign-On (SSO) and authentication providers settings </p>
</div> </div>
<div class="ui basic segment">
<div class="ui yellow message">
<div class="header">
Experimental Feature
</div>
<p>Please note that this feature is still in development and may not work as expected.</p>
</div>
</div>
<div class="ui divider"></div> <div class="ui divider"></div>
<div class="ui basic segment"> <div class="ui top attached tabular menu ssoTabs">
<h3>Forward Auth</h3> <a class="item active" data-tab="forward_auth_tab">Forward Auth</a>
<a class="item" data-tab="oauth2_tab">Oauth2</a>
<!-- <a class="item" data-tab="zoraxy_sso_tab">Zoraxy SSO</a> -->
</div>
<div class="ui bottom attached tab segment active" data-tab="forward_auth_tab">
<!-- Forward Auth -->
<h2>Forward Auth</h2>
<p>Configuration settings for the Forward Auth provider.</p> <p>Configuration settings for the Forward Auth provider.</p>
<p>The Forward Auth provider makes a subrequest to an authorization server that supports Forward Auth, then either:</p> <p>The Forward Auth provider makes a subrequest to an authorization server that supports Forward Auth, then either:</p>
<ul> <ul>
@ -86,10 +83,10 @@
</div> </div>
<button class="ui basic button" type="submit"><i class="green check icon"></i> Apply Change</button> <button class="ui basic button" type="submit"><i class="green check icon"></i> Apply Change</button>
</form> </form>
</div> </div>
<div class="ui divider"></div> <div class="ui bottom attached tab segment" data-tab="oauth2_tab">
<div class="ui basic segment"> <!-- Oauth 2 -->
<h3>OAuth 2.0</h3> <h2>OAuth 2.0</h2>
<p>Configuration settings for OAuth 2.0 authentication provider.</p> <p>Configuration settings for OAuth 2.0 authentication provider.</p>
<form class="ui form" action="#" id="oauth2Settings"> <form class="ui form" action="#" id="oauth2Settings">
@ -134,11 +131,18 @@
</div> </div>
<button class="ui basic button" type="submit"><i class="green check icon"></i> Apply Change</button> <button class="ui basic button" type="submit"><i class="green check icon"></i> Apply Change</button>
</form> </form>
</div> </div>
<div class="ui divider"></div> <div class="ui bottom attached tab segment" data-tab="zoraxy_sso_tab">
<!-- Zoraxy SSO -->
<h3>Zoraxy SSO</h3>
<p>Configuration settings for Zoraxy SSO provider.</p>
<p>Currently not implemented.</p>
</div>
</div> </div>
<script> <script>
$(".ssoTabs .item").tab();
$(document).ready(function() { $(document).ready(function() {
/* Load forward-auth settings from backend */ /* Load forward-auth settings from backend */
$.cjax({ $.cjax({
@ -147,11 +151,31 @@
dataType: 'json', dataType: 'json',
success: function(data) { success: function(data) {
$('#forwardAuthAddress').val(data.address); $('#forwardAuthAddress').val(data.address);
$('#forwardAuthResponseHeaders').val(data.responseHeaders.join(",")); if (data.responseHeaders != null) {
$('#forwardAuthResponseClientHeaders').val(data.responseClientHeaders.join(",")); $('#forwardAuthResponseHeaders').val(data.responseHeaders.join(","));
$('#forwardAuthRequestHeaders').val(data.requestHeaders.join(",")); } else {
$('#forwardAuthRequestIncludedCookies').val(data.requestIncludedCookies.join(",")); $('#forwardAuthResponseHeaders').val("");
$('#forwardAuthRequestExcludedCookies').val(data.requestExcludedCookies.join(",")); }
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) { error: function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching SSO settings:', textStatus, errorThrown); console.error('Error fetching SSO settings:', textStatus, errorThrown);

View File

@ -16,6 +16,7 @@
<th>Target Address</th> <th>Target Address</th>
<th>Mode</th> <th>Mode</th>
<th>Timeout (s)</th> <th>Timeout (s)</th>
<th>Enable Logging</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
@ -73,6 +74,22 @@
<small>Forward UDP request on this listening socket</small></label> <small>Forward UDP request on this listening socket</small></label>
</div> </div>
</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="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 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> <button class="ui basic red button" onclick="event.preventDefault(); cancelStreamProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
@ -195,6 +212,10 @@
modeText.push("UDP") modeText.push("UDP")
} }
if (config.UseProxyProtocol){
modeText.push("ProxyProtocol V1")
}
modeText = modeText.join(" & ") modeText = modeText.join(" & ")
var thisConfig = encodeURIComponent(JSON.stringify(config)); var thisConfig = encodeURIComponent(JSON.stringify(config));
@ -207,6 +228,10 @@
row.append($('<td>').text(config.ProxyTargetAddr)); row.append($('<td>').text(config.ProxyTargetAddr));
row.append($('<td>').text(modeText)); row.append($('<td>').text(modeText));
row.append($('<td>').text(config.Timeout)); 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(` row.append($('<td>').html(`
${startButton} ${startButton}
<button onclick="editTCPProxyConfig('${config.UUID}');" class="ui circular basic mini icon button" title="Edit Config"><i class="edit icon"></i></button> <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"); $(checkboxEle).checkbox("set unchecked");
} }
return; 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"){ }else if (key == "ListeningAddress"){
field = $("#streamProxyForm input[name=listenAddr]"); field = $("#streamProxyForm input[name=listenAddr]");
}else if (key == "ProxyTargetAddr"){ }else if (key == "ProxyTargetAddr"){
@ -301,6 +342,8 @@
proxyAddr: $("#streamProxyForm input[name=proxyAddr]").val().trim(), proxyAddr: $("#streamProxyForm input[name=proxyAddr]").val().trim(),
useTCP: $("#streamProxyForm input[name=useTCP]")[0].checked , useTCP: $("#streamProxyForm input[name=useTCP]")[0].checked ,
useUDP: $("#streamProxyForm input[name=useUDP]")[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()), timeout: parseInt($("#streamProxyForm input[name=timeout]").val().trim()),
}, },
success: function(response) { success: function(response) {

View File

@ -343,7 +343,9 @@
} }
$(editorSideWrapper).each(function(){ $(editorSideWrapper).each(function(){
$(this)[0].contentWindow.setDarkTheme(false); if ($(this)[0].contentWindow.setDarkTheme){
$(this)[0].contentWindow.setDarkTheme(false);
}
}) })
if ($("#pluginContextLoader").is(":visible")){ if ($("#pluginContextLoader").is(":visible")){
@ -356,7 +358,9 @@
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(true); $(".sideWrapper iframe")[0].contentWindow.setDarkTheme(true);
} }
$(editorSideWrapper).each(function(){ $(editorSideWrapper).each(function(){
$(this)[0].contentWindow.setDarkTheme(true); if ($(this)[0].contentWindow.setDarkTheme){
$(this)[0].contentWindow.setDarkTheme(true);
}
}) })
if ($("#pluginContextLoader").is(":visible")){ if ($("#pluginContextLoader").is(":visible")){
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(true); $("#pluginContextLoader")[0].contentWindow.setDarkTheme(true);